Skip to content

Commit a6f9994

Browse files
committed
fix parsing of gpg --list-public-keys output
1 parent ed8c024 commit a6f9994

File tree

8 files changed

+155
-155
lines changed

8 files changed

+155
-155
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Removed
1111

1212
- no longer support anything lower than Node.js 10.x
13+
14+
### Fixed
15+
16+
- fix parsing output from `gpg --list-public-keys --textmode`

bin/list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ git-crypt-users-list: lists git-crypt users according to your GNUPG keyring
2525
}
2626

2727
const details = [keyId, await getUsernames(keyId)];
28-
const parsedKey = parsePublicKeys(result);
28+
const parsedKey = await parsePublicKeys(result);
2929

3030
const hasRevokedUser = parsedKey.users.some(
3131
user =>

bin/remove.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const cwd = process.cwd();
6060
if (!publicKey) {
6161
return false;
6262
}
63-
const parsedKey = parsePublicKeys(publicKey);
63+
const parsedKey = await parsePublicKeys(publicKey);
6464
if (parsedKey.revocationSignature) {
6565
return false; // revoked!
6666
}

bin/rotate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const cwd = process.cwd();
5858
if (!publicKey) {
5959
return false;
6060
}
61-
const parsedKey = parsePublicKeys(publicKey);
61+
const parsedKey = await parsePublicKeys(publicKey);
6262
if (parsedKey.revocationSignature) {
6363
return false; // revoked!
6464
}

lib/gpg.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const execa = require('execa');
55

66
const { HKP, key } = require('openpgp');
7+
global.fetch = require('node-fetch'); // needed for opengpg lookups
78

89
/* :: import type { ParsedKey } from '../types.js' */
910

@@ -21,7 +22,7 @@ async function getLocalPublicKey(keyId /* : string */) {
2122
}
2223

2324
async function getUsernames(keyId /* : string */) {
24-
const parsedKey = parsePublicKeys(await getLocalPublicKey(keyId));
25+
const parsedKey = await parsePublicKeys(await getLocalPublicKey(keyId));
2526
return (
2627
parsedKey.users
2728
.map(user => (user && user.userId && user.userId.userid) || 'unknown')
@@ -32,12 +33,7 @@ async function getUsernames(keyId /* : string */) {
3233
async function listKnownPublicKeys() {
3334
const { stdout } = await execa('gpg', ['--list-public-keys', '--textmode']);
3435

35-
// stdout starts with:
36-
// /Users/$USER/.gnupg/pubring.kbx
37-
// ---------------------------------
38-
const [, output] = stdout.split('---------------------------------');
39-
40-
return output
36+
return stdout
4137
.split('\n\n')
4238
.map(entry => {
4339
const [keyId] = entry.match(FINGERPRINT_REGEXP) || [];
@@ -50,8 +46,10 @@ async function lookupPublicKey(keyId /* : string */) {
5046
return (await mit.lookup({ keyId })) || (await pgp.lookup({ keyId }));
5147
}
5248

53-
function parsePublicKeys(asciiArmor /* :? string */) /* : ParsedKey */ {
54-
const { keys } = key.readArmored(asciiArmor);
49+
async function parsePublicKeys(
50+
asciiArmor /* :? string */,
51+
) /* : Promise<ParsedKey> */ {
52+
const { keys = [] } = await key.readArmored(asciiArmor);
5553
if (keys.length !== 1) {
5654
throw new TypeError(
5755
'parsePublicKeys() expects argument to contain a single public key',

lib/gpg.test.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* eslint-env jest */
2+
'use strict';
3+
4+
const { mkdtemp } = require('fs');
5+
const { tmpdir } = require('os');
6+
const { join: joinPath } = require('path');
7+
const { promisify } = require('util');
8+
9+
const execa = require('execa');
10+
const rimraf = require('rimraf');
11+
12+
const {
13+
getLocalPublicKey,
14+
getUsernames,
15+
listKnownPublicKeys,
16+
lookupPublicKey,
17+
parsePublicKeys,
18+
} = require('./gpg');
19+
20+
describe('gpg', () => {
21+
let tempDir;
22+
23+
afterEach(async () => {
24+
if (tempDir) {
25+
await promisify(rimraf)(tempDir);
26+
}
27+
delete process.env.GNUPGHOME;
28+
});
29+
30+
beforeEach(async () => {
31+
tempDir = await promisify(mkdtemp)(
32+
joinPath(tmpdir(), 'git-crypt-test--gpg-'),
33+
);
34+
process.env.GNUPGHOME = tempDir;
35+
await execa('gpg', ['--generate-key', '--batch'], {
36+
input: [
37+
'Key-Type: default',
38+
'Subkey-Type: default',
39+
'Passphrase: alice',
40+
'Name-Real: Alice',
41+
'Name-Email: alice@example.local',
42+
'%commit',
43+
].join('\n'),
44+
});
45+
await execa('gpg', ['--generate-key', '--batch'], {
46+
input: [
47+
'Key-Type: default',
48+
'Subkey-Type: default',
49+
'Passphrase: bob',
50+
'Name-Real: Bob',
51+
'Name-Email: bob@example.local',
52+
'%commit',
53+
].join('\n'),
54+
});
55+
});
56+
57+
describe('getLocalPublicKey()', () => {
58+
it('finds ASCII Armor for Alice or Bob', async () => {
59+
const keyIds = await listKnownPublicKeys();
60+
expect(keyIds).toHaveLength(2);
61+
62+
const [keyId] = keyIds;
63+
const got = await getLocalPublicKey(keyId);
64+
expect(got).toMatch('-BEGIN PGP PUBLIC KEY BLOCK-');
65+
expect(got).toMatch('-END PGP PUBLIC KEY BLOCK-');
66+
});
67+
});
68+
69+
describe('getUsernames()', () => {
70+
it('finds usernames for Alice and Bob', async () => {
71+
const keyIds = await listKnownPublicKeys();
72+
expect(keyIds).toHaveLength(2);
73+
74+
const got = await Promise.all(keyIds.map(getUsernames));
75+
expect(got).toHaveLength(2);
76+
expect(got).toContain('Alice <alice@example.local>');
77+
expect(got).toContain('Bob <bob@example.local>');
78+
});
79+
});
80+
81+
describe('listKnownPublicKeys()', () => {
82+
it('finds public keys for Alice and Bob', async () => {
83+
const got = await listKnownPublicKeys();
84+
expect(got).toHaveLength(2);
85+
for (const keyId of got) {
86+
expect(typeof keyId).toBe('string');
87+
}
88+
89+
const unique = Array.from(new Set(got));
90+
expect(unique).toHaveLength(2);
91+
});
92+
});
93+
94+
describe('lookupPublicKey()', () => {
95+
it('finds the public key for Hashicorp', async () => {
96+
try {
97+
const got = await lookupPublicKey(
98+
'91A6E7F85D05C65630BEF18951852D87348FFC4C',
99+
);
100+
expect(got).toMatch('-BEGIN PGP PUBLIC KEY BLOCK-');
101+
expect(got).toMatch('-END PGP PUBLIC KEY BLOCK-');
102+
} catch (err) {
103+
// noop
104+
// we're just testing the result when there is one,
105+
// we don't care if the keyserver is having a bad day
106+
}
107+
});
108+
});
109+
110+
describe('parsePublicKeys()', () => {
111+
it('parses ASCII Armor for Alice or Bob', async () => {
112+
const keyIds = await listKnownPublicKeys();
113+
expect(keyIds).toHaveLength(2);
114+
115+
const [keyId] = keyIds;
116+
const asciiArmor = await getLocalPublicKey(keyId);
117+
const got = await parsePublicKeys(asciiArmor);
118+
expect(typeof got.users[0].userId.userid).toBe('string');
119+
});
120+
121+
it('throws when not given a valid ASCII Armor', async () => {
122+
return expect(parsePublicKeys('')).rejects.toMatchObject(
123+
new TypeError(
124+
'parsePublicKeys() expects argument to contain a single public key',
125+
),
126+
);
127+
});
128+
});
129+
});

0 commit comments

Comments
 (0)