diff --git a/CHANGELOG.md b/CHANGELOG.md index a2d435ff..df57a011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## In master - [General] Add support for building a commonjs bundle +- [Keys] Replace `KeyManager#loadAllKeys` with `loadKey` and `loadAllKeyIds`. + This fixes a bug where a user wouldn't be able to save multiple keys with + different passwords. - [Transfers] Normalize some anchor transactions that almost meet SEP-24 spec - [Transfers] Support fetching single transactions by their stellar id or external id diff --git a/src/KeyManager.test.ts b/src/KeyManager.test.ts index c44262fb..004be374 100644 --- a/src/KeyManager.test.ts +++ b/src/KeyManager.test.ts @@ -6,6 +6,7 @@ import { KeyType } from "./constants/keys"; import { KeyManager } from "./KeyManager"; import { IdentityEncrypter } from "./plugins/IdentityEncrypter"; import { MemoryKeyStore } from "./plugins/MemoryKeyStore"; +import { ScryptEncrypter } from "./plugins/ScryptEncrypter"; // tslint:disable-next-line describe("KeyManager", function() { @@ -20,18 +21,20 @@ describe("KeyManager", function() { clock.restore(); }); - test("Set an ID of one's own", async () => { + test("Save an ID of one's own", async () => { const testStore = new MemoryKeyStore(); const testKeyManager = new KeyManager({ keyStore: testStore, }); + const id = "this is a very good id, and I like it"; + testKeyManager.registerEncrypter(IdentityEncrypter); const password = "test"; const metadata = await testKeyManager.storeKey({ key: { - id: "this is a very good id, and I like it", + id, type: KeyType.plaintextKey, publicKey: "AVACYN", privateKey: "ARCHANGEL", @@ -41,24 +44,67 @@ describe("KeyManager", function() { }); expect(metadata).toEqual({ - id: "this is a very good id, and I like it", + id, }); - expect(await testKeyManager.loadAllKeys(password)).toEqual([ - { - id: "this is a very good id, and I like it", - privateKey: "ARCHANGEL", + expect(await testKeyManager.loadAllKeyIds()).toEqual([id]); + + expect(await testKeyManager.loadKey(id, password)).toEqual({ + id, + type: KeyType.plaintextKey, + publicKey: "AVACYN", + privateKey: "ARCHANGEL", + }); + }); + + test("Save and remove an ID of one's own", async () => { + const testStore = new MemoryKeyStore(); + const testKeyManager = new KeyManager({ + keyStore: testStore, + }); + + const id = "this is a very good id, and I like it"; + + testKeyManager.registerEncrypter(IdentityEncrypter); + + const password = "test"; + const metadata = await testKeyManager.storeKey({ + key: { + id, + type: KeyType.plaintextKey, publicKey: "AVACYN", - type: "plaintextKey", + privateKey: "ARCHANGEL", }, - ]); + password, + encrypterName: "IdentityEncrypter", + }); + + expect(metadata).toEqual({ + id, + }); + + expect(await testKeyManager.loadAllKeyIds()).toEqual([id]); + + expect(await testKeyManager.loadKey(id, password)).toEqual({ + id, + type: KeyType.plaintextKey, + publicKey: "AVACYN", + privateKey: "ARCHANGEL", + }); await testKeyManager.removeKey(metadata.id); - expect(await testKeyManager.loadAllKeys(password)).toEqual([]); + try { + await testKeyManager.loadKey(id, password); + expect( + "The function should have thrown but didn't, the test failed!", + ).toBe(null); + } catch (e) { + expect(e.toString()).toContain("Key not found"); + } }); - test("Save / remove keys", async () => { + test("Save keys", async () => { const testStore = new MemoryKeyStore(); const testKeyManager = new KeyManager({ keyStore: testStore, @@ -81,18 +127,54 @@ describe("KeyManager", function() { id: "0.5", }); - expect(await testKeyManager.loadAllKeys(password)).toEqual([ - { - id: "0.5", - privateKey: "ARCHANGEL", + expect(await testKeyManager.loadKey("0.5", password)).toEqual({ + id: "0.5", + privateKey: "ARCHANGEL", + publicKey: "AVACYN", + type: "plaintextKey", + }); + }); + + test("Save / remove keys", async () => { + const testStore = new MemoryKeyStore(); + const testKeyManager = new KeyManager({ + keyStore: testStore, + }); + + testKeyManager.registerEncrypter(IdentityEncrypter); + + const password = "test"; + const metadata = await testKeyManager.storeKey({ + key: { + type: KeyType.plaintextKey, publicKey: "AVACYN", - type: "plaintextKey", + privateKey: "ARCHANGEL", }, - ]); + password, + encrypterName: "IdentityEncrypter", + }); + + expect(metadata).toEqual({ + id: "0.5", + }); + + expect(await testKeyManager.loadKey("0.5", password)).toEqual({ + id: "0.5", + privateKey: "ARCHANGEL", + publicKey: "AVACYN", + type: "plaintextKey", + }); await testKeyManager.removeKey(metadata.id); - expect(await testKeyManager.loadAllKeys(password)).toEqual([]); + try { + await testKeyManager.loadKey("0.5", password); + expect( + "The function should have thrown but didn't, the test failed!", + ).toBe(null); + } catch (e) { + expect(e.toString()).toContain("Key not found"); + } }); test("Sign transactions", async () => { @@ -265,3 +347,144 @@ describe("KeyManager", function() { }); }); }); + +describe("KeyManager Scrypt", () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(666); + mockRandomForEach(0.5); + }); + + afterEach(() => { + clock.restore(); + }); + + test("Save / remove keys", async () => { + const testStore = new MemoryKeyStore(); + const testKeyManager = new KeyManager({ + keyStore: testStore, + }); + + testKeyManager.registerEncrypter(ScryptEncrypter); + + const password = "test"; + const metadata = await testKeyManager.storeKey({ + key: { + type: KeyType.plaintextKey, + publicKey: "AVACYN", + privateKey: "ARCHANGEL", + }, + password, + encrypterName: ScryptEncrypter.name, + }); + + expect(metadata).toEqual({ + id: "0.5", + }); + + expect(await testKeyManager.loadKey("0.5", password)).toEqual({ + id: "0.5", + privateKey: "ARCHANGEL", + publicKey: "AVACYN", + type: "plaintextKey", + }); + + try { + await testKeyManager.loadKey( + "0.5", + "i don't know the password but I'm hoping the decrypter works anyway", + ); + expect( + "The function should have thrown but didn't, the test failed!", + ).toBe(null); + } catch (e) { + expect(e.toString()).toContain("Couldn’t decrypt key"); + } + + await testKeyManager.removeKey(metadata.id); + + try { + await testKeyManager.loadKey("0.5", password); + expect( + "The function should have thrown but didn't, the test failed!", + ).toBe(null); + } catch (e) { + expect(e.toString()).toContain("Key not found"); + } + }); +}); + +describe("KeyManager Scrypt, multiple keys with different passwords", () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(666); + }); + + afterEach(() => { + clock.restore(); + }); + + test("Save multiple keys", async () => { + const testStore = new MemoryKeyStore(); + const testKeyManager = new KeyManager({ + keyStore: testStore, + }); + + testKeyManager.registerEncrypter(ScryptEncrypter); + + const password1 = "test1"; + const metadata1 = await testKeyManager.storeKey({ + key: { + id: "key1", + type: KeyType.plaintextKey, + publicKey: "AVACYN1", + privateKey: "ARCHANGEL1", + }, + password: password1, + encrypterName: ScryptEncrypter.name, + }); + + expect(metadata1).toEqual({ + id: "key1", + }); + + expect(await testKeyManager.loadKey("key1", password1)).toEqual({ + id: "key1", + privateKey: "ARCHANGEL1", + publicKey: "AVACYN1", + type: "plaintextKey", + }); + + const password2 = "test1"; + const metadata2 = await testKeyManager.storeKey({ + key: { + id: "key2", + type: KeyType.plaintextKey, + publicKey: "AVACYN2", + privateKey: "ARCHANGEL2", + }, + password: password2, + encrypterName: ScryptEncrypter.name, + }); + + expect(metadata2).toEqual({ + id: "key2", + }); + + expect(await testKeyManager.loadKey("key2", password2)).toEqual({ + id: "key2", + privateKey: "ARCHANGEL2", + publicKey: "AVACYN2", + type: "plaintextKey", + }); + + expect(await testKeyManager.loadKey("key1", password1)).toEqual({ + id: "key1", + privateKey: "ARCHANGEL1", + publicKey: "AVACYN1", + type: "plaintextKey", + }); + }); +}); diff --git a/src/KeyManager.ts b/src/KeyManager.ts index 0adb5c2d..3ea54de1 100644 --- a/src/KeyManager.ts +++ b/src/KeyManager.ts @@ -156,26 +156,51 @@ export class KeyManager { } /** - * List information about stored keys + * Load and decrypt one key, given its id. * - * @returns a list of all stored keys + * @returns Decrypted key */ - public async loadAllKeys(password: string): Promise { + public async loadKey(id: string, password: string): Promise { const encryptedKeys: EncryptedKey[] = await this.keyStore.loadAllKeys(); - const keys = []; + const keys = encryptedKeys.filter((k) => k.id === id); - while (encryptedKeys.length) { - const encryptedKey = encryptedKeys.shift() as EncryptedKey; - const encrypter = this.encrypterMap[encryptedKey.encrypterName]; - const key = await encrypter.decryptKey({ + if (!keys.length) { + throw new Error(`Key not found with id '${id}'.`); + } + + if (keys.length > 1) { + throw new Error( + `Too many keys found with id '${id}', that’s not supposed to happen!`, + ); + } + + const encryptedKey = keys[0]; + const encrypter = this.encrypterMap[encryptedKey.encrypterName]; + + let key; + + try { + key = await encrypter.decryptKey({ encryptedKey, password, }); - - keys.push(key); + } catch (e) { + throw new Error( + `Couldn’t decrypt key '${id}' with the supplied password.`, + ); } - return keys; + return key; + } + + /** + * Get a list of all stored key ids. + * + * @returns List of ids + */ + public async loadAllKeyIds(): Promise { + const encryptedKeys: EncryptedKey[] = await this.keyStore.loadAllKeys(); + return encryptedKeys.map((key) => key.id); } /**