Skip to content

Commit 8c550eb

Browse files
committed
KeyEditor: Implement deluid and passwd commands
1 parent b0701ff commit 8c550eb

2 files changed

Lines changed: 173 additions & 18 deletions

File tree

Crypt/GPG/KeyEditor.php

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
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");

tests/KeyEditorTest.php

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@
3030
*/
3131

3232
require_once 'TestCase.php';
33-
require_once 'Crypt/GPG/Key.php';
34-
require_once 'Crypt/GPG/UserId.php';
35-
require_once 'Crypt/GPG/SubKey.php';
3633
require_once 'Crypt/GPG/KeyEditor.php';
3734

3835
/**
@@ -107,18 +104,92 @@ public function testAddUserId()
107104
$userIds = array_filter($userIds, function ($id) use ($user) { return $id->getName() == $user->getName(); });
108105
$this->assertCount(0, $userIds);
109106

107+
// Test editing a key that has no secret key
108+
$this->expectException('Crypt_GPG_Exception');
109+
$keyEditor->edit('public-only@example.com', 'test')->addUserId($user)->save();
110+
110111
// Test invalid password
111112
$this->expectException('Crypt_GPG_BadPassphraseException');
112-
$keyEditor->edit('second-keypair@example.com', 'wrong')
113-
->addUserId($user)
114-
->save();
113+
$keyEditor->edit('second-keypair@example.com', 'wrong')->addUserId($user)->save();
115114
}
116115

117116
/**
118117
* Test `deluid` command
119118
*/
120119
public function testDeleteUserId()
121120
{
122-
$this->markTestIncomplete();
121+
$keyEditor = $this->gpg->getKeyEditor();
122+
123+
// First add some users to the key
124+
$user1 = new Crypt_GPG_UserId([
125+
'name' => 'Alice',
126+
'comment' => 'shipping',
127+
'email' => 'alice@example.com'
128+
]);
129+
130+
$user2 = new Crypt_GPG_UserId([
131+
'name' => 'John',
132+
'comment' => '',
133+
'email' => 'john@example.com'
134+
]);
135+
136+
$user3 = new Crypt_GPG_UserId([
137+
'name' => '',
138+
'comment' => '',
139+
'email' => 'john@example.com'
140+
]);
141+
142+
$keyEditor->edit('second-keypair@example.com', 'test2')
143+
->addUserId($user1)
144+
->addUserId($user2)
145+
->addUserId($user3)
146+
->save();
147+
148+
// Test deleting user with name, comment and email
149+
$keyEditor->edit('second-keypair@example.com', 'test2')->deleteUserId($user1)->save();
150+
151+
$keys = $this->gpg->getKeys('second-keypair@example.com');
152+
$userIds = $keys[0]->getUserIds();
153+
$this->assertCount(3, $userIds);
154+
$userIds = array_filter($userIds, function ($id) use ($user1) { return $id->getEmail() != $user1->getEmail(); });
155+
$this->assertCount(3, $userIds);
156+
157+
// Test deleting users with no name or no comment
158+
$keyEditor->edit('second-keypair@example.com', 'test2')
159+
->deleteUserId($user2)
160+
->deleteUserId($user3)
161+
->save();
162+
163+
$keys = $this->gpg->getKeys('second-keypair@example.com');
164+
$userIds = $keys[0]->getUserIds();
165+
$this->assertCount(1, $userIds);
166+
$userIds = array_filter($userIds, function ($id) use ($user1) { return $id->getEmail() != $user1->getEmail(); });
167+
$this->assertCount(1, $userIds);
168+
169+
// Test deleting the last user
170+
$user = new Crypt_GPG_UserId('Second Keypair Test Key (do not encrypt important data with this key) <second-keypair@example.com>');
171+
$this->expectException('Crypt_GPG_Exception');
172+
$keyEditor->edit('second-keypair@example.com', 'test2')->deleteUserId($user)->save();
173+
174+
// Test deleting the last user
175+
$user = new Crypt_GPG_UserId('<unknown-keypair@example.com>');
176+
$this->expectException('Crypt_GPG_Exception');
177+
$keyEditor->edit('second-keypair@example.com', 'test2')->deleteUserId($user)->save();
178+
}
179+
180+
/**
181+
* Test `passwd` command
182+
*/
183+
public function testPasswd()
184+
{
185+
$keyEditor = $this->gpg->getKeyEditor();
186+
$keyEditor->edit('first-keypair@example.com', 'test1')->passwd('new pass')->save();
187+
188+
// Assert the new password in fact works
189+
$keyEditor->edit('first-keypair@example.com', 'new pass')
190+
->addUserId($user = new Crypt_GPG_UserId('alice@example.com'))
191+
->save();
192+
193+
$this->assertTrue(true);
123194
}
124195
}

0 commit comments

Comments
 (0)