Skip to content

Commit c695bc6

Browse files
Merge pull request #1730 from input-output-hk/feat/add-encryption_param_to_keyagent
feat(key-management): allow injecting custom root private key encryption in InMemoryKeyAgent
2 parents a018c19 + 6bcba11 commit c695bc6

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

packages/key-management/src/InMemoryKeyAgent.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
KeyAgentType,
99
KeyPair,
1010
KeyPurpose,
11+
PassphraseEncryption,
1112
SerializableInMemoryKeyAgentData,
1213
SignBlobResult,
1314
SignTransactionContext,
@@ -30,6 +31,8 @@ import { KeyAgentBase } from './KeyAgentBase';
3031
import { emip3decrypt, emip3encrypt } from './emip3';
3132
import uniqBy from 'lodash/uniqBy.js';
3233

34+
const defaultRootPrivateKeyEncryption: PassphraseEncryption = { decrypt: emip3decrypt, encrypt: emip3encrypt };
35+
3336
export interface InMemoryKeyAgentProps extends Omit<SerializableInMemoryKeyAgentData, '__typename'> {
3437
getPassphrase: GetPassphrase;
3538
}
@@ -53,10 +56,12 @@ const getPassphraseRethrowTypedError = async (getPassphrase: GetPassphrase) => {
5356

5457
export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent {
5558
readonly #getPassphrase: GetPassphrase;
59+
readonly #rootPrivateKeyEncryption: PassphraseEncryption;
5660

5761
constructor({ getPassphrase, ...serializableData }: InMemoryKeyAgentProps, dependencies: KeyAgentDependencies) {
5862
super({ ...serializableData, __typename: KeyAgentType.InMemory }, dependencies);
5963
this.#getPassphrase = getPassphrase;
64+
this.#rootPrivateKeyEncryption = dependencies.rootPrivateKeyEncryption ?? defaultRootPrivateKeyEncryption;
6065
}
6166

6267
async signBlob({ index, role: type }: AccountKeyDerivationPath, blob: HexBlob): Promise<SignBlobResult> {
@@ -109,7 +114,11 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent {
109114
const entropy = Buffer.from(mnemonicWordsToEntropy(mnemonicWords), 'hex');
110115
const rootPrivateKey = dependencies.bip32Ed25519.fromBip39Entropy(entropy, mnemonic2ndFactorPassphrase);
111116
const passphrase = await getPassphraseRethrowTypedError(getPassphrase);
112-
const encryptedRootPrivateKey = await emip3encrypt(Buffer.from(rootPrivateKey, 'hex'), passphrase);
117+
const rootPrivateKeyEncryption = dependencies.rootPrivateKeyEncryption ?? defaultRootPrivateKeyEncryption;
118+
const encryptedRootPrivateKey = await rootPrivateKeyEncryption.encrypt(
119+
Buffer.from(rootPrivateKey, 'hex'),
120+
passphrase
121+
);
113122
const accountPrivateKey = await deriveAccountPrivateKey({
114123
accountIndex,
115124
bip32Ed25519: dependencies.bip32Ed25519,
@@ -176,7 +185,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent {
176185
let decryptedRootKeyBytes: Uint8Array;
177186

178187
try {
179-
decryptedRootKeyBytes = await emip3decrypt(
188+
decryptedRootKeyBytes = await this.#rootPrivateKeyEncryption.decrypt(
180189
new Uint8Array((this.serializableData as SerializableInMemoryKeyAgentData).encryptedRootPrivateKeyBytes),
181190
passphrase
182191
);

packages/key-management/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,21 @@ export enum CommunicationType {
6868
Node = 'node'
6969
}
7070

71+
/**
72+
* Pluggable passphrase-based encryption for secrets at rest (e.g. the root
73+
* private key). `encrypt` and `decrypt` are inverses for a given passphrase.
74+
* Defaults to EMIP-003 when not provided to a key agent.
75+
*/
76+
export interface PassphraseEncryption {
77+
encrypt: (data: Uint8Array, passphrase: Uint8Array) => Promise<Uint8Array>;
78+
decrypt: (encrypted: Uint8Array, passphrase: Uint8Array) => Promise<Uint8Array>;
79+
}
80+
7181
export interface KeyAgentDependencies {
7282
logger: Logger;
7383
bip32Ed25519: Crypto.Bip32Ed25519;
84+
/** Overrides root private key encryption at rest. Defaults to EMIP-003. */
85+
rootPrivateKeyEncryption?: PassphraseEncryption;
7486
}
7587

7688
export interface AccountAddressDerivationPath {

packages/key-management/test/InMemoryKeyAgent.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
KeyPurpose,
77
KeyRole,
88
SerializableInMemoryKeyAgentData,
9+
emip3decrypt,
910
util
1011
} from '../src';
1112
import { Bip32Ed25519 } from '@cardano-sdk/crypto';
@@ -16,6 +17,11 @@ import { dummyLogger } from 'ts-log';
1617
jest.mock('../src/util/ownSignatureKeyPaths');
1718
const { ownSignatureKeyPaths } = jest.requireMock('../src/util/ownSignatureKeyPaths');
1819

20+
const makeEncryption = () => ({
21+
decrypt: jest.fn(async (encrypted: Uint8Array) => encrypted.slice(1)),
22+
encrypt: jest.fn(async (data: Uint8Array) => Uint8Array.from([170, ...data]))
23+
});
24+
1925
describe('InMemoryKeyAgent', () => {
2026
let keyAgent: InMemoryKeyAgent;
2127
let getPassphrase: jest.Mock;
@@ -66,6 +72,38 @@ describe('InMemoryKeyAgent', () => {
6672
expect(await saferKeyAgent.exportRootPrivateKey()).not.toEqual(await keyAgent.exportRootPrivateKey());
6773
});
6874

75+
describe('rootPrivateKeyEncryption injection', () => {
76+
it('uses the injected encryption to encrypt the root private key', async () => {
77+
const encryption = makeEncryption();
78+
const agent = await InMemoryKeyAgent.fromBip39MnemonicWords(
79+
{ chainId: Cardano.ChainIds.Preview, getPassphrase, mnemonicWords },
80+
{ bip32Ed25519, logger: dummyLogger, rootPrivateKeyEncryption: encryption }
81+
);
82+
83+
expect(encryption.encrypt).toHaveBeenCalledTimes(1);
84+
const { encryptedRootPrivateKeyBytes } = agent.serializableData as SerializableInMemoryKeyAgentData;
85+
expect(encryptedRootPrivateKeyBytes[0]).toBe(170);
86+
});
87+
88+
it('uses the injected encryption to decrypt, recovering the same root key as the default', async () => {
89+
const encryption = makeEncryption();
90+
const agent = await InMemoryKeyAgent.fromBip39MnemonicWords(
91+
{ chainId: Cardano.ChainIds.Preview, getPassphrase, mnemonicWords },
92+
{ bip32Ed25519, logger: dummyLogger, rootPrivateKeyEncryption: encryption }
93+
);
94+
95+
const rootPrivateKey = await agent.exportRootPrivateKey();
96+
expect(encryption.decrypt).toHaveBeenCalled();
97+
expect(rootPrivateKey).toEqual(await keyAgent.exportRootPrivateKey());
98+
});
99+
100+
it('defaults to EMIP-003 when no encryption is injected', async () => {
101+
const { encryptedRootPrivateKeyBytes } = keyAgent.serializableData as SerializableInMemoryKeyAgentData;
102+
const decrypted = await emip3decrypt(new Uint8Array(encryptedRootPrivateKeyBytes), Buffer.from('password'));
103+
expect(Buffer.from(decrypted).toString('hex')).toEqual(await keyAgent.exportRootPrivateKey());
104+
});
105+
});
106+
69107
describe('serializableData', () => {
70108
let serializableData: SerializableInMemoryKeyAgentData;
71109

0 commit comments

Comments
 (0)