22
33/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
44
5- require_once 'Crypt/GPG/Engine .php ' ;
5+ require_once 'Crypt/GPGAbstract .php ' ;
66
77/**
88 * A class for editing keys (using GnuPG interactive --key-edit shell)
@@ -182,19 +182,85 @@ public function addUserId(Crypt_GPG_UserId $userid)
182182 'passphrase.enter ' => $ this ->passphrase ,
183183 ];
184184
185- $ this ->_write ('adduid ' )->_read ($ handlers , ['keyedit.prompt ' ]);
185+ $ output = $ this ->_write ('adduid ' )->_read ($ handlers , ['keyedit.prompt ' ]);
186+
187+ if (strpos ($ output , 'Need the secret key to do this ' )) {
188+ $ this ->_close ();
189+ throw new Crypt_GPG_Exception ('Failed to add a user. No secret key found. ' );
190+ }
186191
187192 return $ this ;
188193 }
189194
190195 /**
191196 * Delete a user identity from a key (`deluid`).
192197 *
198+ * @param Crypt_GPG_UserId $userid User identity to delete
199+ * @param bool $by_email Delete all identities with specified email address
200+ *
201+ * @return Crypt_GPG_KeyEditor The current object, for fluent interface.
202+ */
203+ public function deleteUserId (Crypt_GPG_UserId $ userid , $ by_email = false )
204+ {
205+ // Find the identity index (`uid 0`), call `uid X`, call `deluid`.
206+ $ output = $ this ->_write ('list ' )->_read ([], ['keyedit.prompt ' ]);
207+
208+ // Process the output to find and match the user entries, and get their ids
209+ $ uids = [];
210+ foreach (explode ("\n" , $ output ) as $ line ) {
211+ if (preg_match ('/^\[[^\]]+\]\s+\(([0-9]+)\)\.?\s+(.*)$/ ' , $ line , $ matches )) {
212+ $ ident = Crypt_GPG_UserId::parse ($ matches [2 ]);
213+ if ((string ) $ ident === (string ) $ userid || ($ by_email && $ ident ->getEmail () === $ userid ->getEmail ())) {
214+ $ uids [] = $ matches [1 ];
215+ }
216+ }
217+ }
218+
219+ if (empty ($ uids )) {
220+ throw new Exception ("No matching users in the key. " );
221+ }
222+
223+ // We'll delete users in order where deletion does not change other IDs
224+ arsort ($ uids , SORT_NUMERIC );
225+
226+ $ handlers = [
227+ 'keyedit.remove.uid.okay ' => true ,
228+ 'passphrase.enter ' => $ this ->passphrase ,
229+ ];
230+
231+ foreach ($ uids as $ uid ) {
232+ $ this ->_write ("uid {$ uid }" )->_read ($ handlers , ['keyedit.prompt ' ]);
233+ $ output = $ this ->_write ('deluid ' )->_read ($ handlers , ['keyedit.prompt ' ]);
234+
235+ if (strpos ($ output , 'You can \'t delete the last ' )) {
236+ $ this ->_close ();
237+ throw new Crypt_GPG_Exception ('Failed to delete user from a key. You can \'t delete the last user. ' );
238+ }
239+ }
240+
241+ return $ this ;
242+ }
243+
244+ /**
245+ * Change a key passphrase (`passwd`).
246+ *
247+ * @param string $passphrase New passphrase
248+ *
193249 * @return Crypt_GPG_KeyEditor The current object, for fluent interface.
194250 */
195- public function deleteUserId ( Crypt_GPG_UserId $ userid )
251+ public function passwd ( $ passphrase )
196252 {
197- // TODO: Find the identity index (`uid 0`), call `uid X`, call `deluid`.
253+ // FIXME: Seems old and new pass use the same 'passphrase.enter' command
254+ // What if the key has no password (or it is in cache)?
255+
256+ $ handlers = [
257+ 'passphrase.enter ' => [$ this ->passphrase , $ passphrase ],
258+ ];
259+
260+ // TODO: This does not seem to work with empty passphrase
261+
262+ $ this ->_write ('passwd ' )->_read ($ handlers , ['keyedit.prompt ' ]);
263+
198264 return $ this ;
199265 }
200266
@@ -205,7 +271,7 @@ public function deleteUserId(Crypt_GPG_UserId $userid)
205271 */
206272 public function quit ()
207273 {
208- $ this ->_write ('quit ' )->_read (['keyedit.save.okay ' => ' N ' ]);
274+ $ this ->_write ('quit ' )->_read (['keyedit.save.okay ' => false ]);
209275 $ this ->_close ();
210276 return $ this ;
211277 }
@@ -238,6 +304,7 @@ private function _close()
238304 $ this ->_debug ("CLOSED GPG SUBPROCESS " );
239305 }
240306
307+ $ this ->passphrase = '' ;
241308 $ this ->process = null ;
242309 $ this ->pipes = [];
243310 }
@@ -247,6 +314,11 @@ private function _close()
247314 */
248315 private function _read ($ handlers = [], $ stop_at = [])
249316 {
317+ if (empty ($ this ->pipes [Crypt_GPG_Engine::FD_ERROR ])) {
318+ $ this ->_close ();
319+ throw new Crypt_GPG_Exception ('The key editor output stream is closed. ' );
320+ }
321+
250322 $ output = '' ;
251323 $ passInput = false ;
252324
@@ -255,17 +327,18 @@ private function _read($handlers = [], $stop_at = [])
255327 $ outputStreams = [];
256328 $ exceptionStreams = [];
257329
258- if (!feof ($ this ->pipes [Crypt_GPG_Engine::FD_ERROR ])) {
330+ if (!empty ( $ this -> pipes [Crypt_GPG_Engine:: FD_ERROR ]) && ! feof ($ this ->pipes [Crypt_GPG_Engine::FD_ERROR ])) {
259331 $ inputStreams [] = $ this ->pipes [Crypt_GPG_Engine::FD_ERROR ];
260332 }
261333
262334 if (count ($ inputStreams ) === 0 ) {
263335 break ;
264336 }
265-
337+
266338 $ ready = stream_select ($ inputStreams , $ outputStreams , $ exceptionStreams , null );
267339
268340 if ($ ready === false || $ ready === 0 ) {
341+ $ this ->_close ();
269342 throw new Crypt_GPG_Exception ('Error selecting stream for communication with GPG subprocess ' );
270343 }
271344
@@ -283,10 +356,17 @@ private function _read($handlers = [], $stop_at = [])
283356 $ token = $ matches [2 ];
284357
285358 if (isset ($ handlers [$ token ])) {
286- if (is_string ($ handlers [$ token ])) {
287- $ this ->_write ($ handlers [$ token ]);
288- } elseif (is_callable ($ handlers [$ token ])) {
289- $ handlers [$ token ]($ token , $ output );
359+ $ handler = $ handlers [$ token ];
360+ if (is_array ($ handler )) {
361+ $ handler = array_shift ($ handlers [$ token ]);
362+ }
363+
364+ if (is_string ($ handler )) {
365+ $ this ->_write ($ handler );
366+ } elseif (is_bool ($ handler )) {
367+ $ this ->_write ($ handler ? 'y ' : 'N ' );
368+ } elseif (is_callable ($ handler )) {
369+ $ handler ($ token , $ output );
290370 }
291371
292372 $ output = '' ;
@@ -316,6 +396,10 @@ private function _read($handlers = [], $stop_at = [])
316396 */
317397 private function _write ($ input )
318398 {
399+ if (empty ($ this ->pipes [Crypt_GPG_Engine::FD_INPUT ]) || feof ($ this ->pipes [Crypt_GPG_Engine::FD_INPUT ])) {
400+ throw new Crypt_GPG_Exception ('The key editor input stream is closed. ' );
401+ }
402+
319403 $ this ->_debug ("< $ input " );
320404
321405 fwrite ($ this ->pipes [Crypt_GPG_Engine::FD_INPUT ], "$ input \n" );
0 commit comments