-
Notifications
You must be signed in to change notification settings - Fork 54
/
InMemoryKeyAgent.ts
141 lines (126 loc) · 5.17 KB
/
InMemoryKeyAgent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import * as errors from './errors';
import {
AccountKeyDerivationPath,
GetPassword,
HexBlob,
KeyAgentType,
SerializableKeyAgentData,
SignBlobResult
} from './types';
import { AuthenticationError } from './errors';
import { CSL, Cardano } from '@cardano-sdk/core';
import { KeyAgentBase } from './KeyAgentBase';
import { emip3decrypt, emip3encrypt } from './emip3';
import { harden, joinMnemonicWords, mnemonicWordsToEntropy, validateMnemonic } from './util';
export interface InMemoryKeyAgentProps {
networkId: Cardano.NetworkId;
accountIndex: number;
encryptedRootPrivateKey: Uint8Array;
getPassword: GetPassword;
}
export interface FromBip39MnemonicWordsProps {
networkId: Cardano.NetworkId;
mnemonicWords: string[];
mnemonic2ndFactorPassphrase?: Uint8Array;
getPassword: GetPassword;
accountIndex?: number;
}
const getPasswordRethrowTypedError = async (getPassword: GetPassword) => {
try {
return await getPassword();
} catch (error) {
throw new AuthenticationError('Failed to enter password', error);
}
};
export class InMemoryKeyAgent extends KeyAgentBase {
readonly #networkId: Cardano.NetworkId;
readonly #accountIndex: number;
readonly #encryptedRootPrivateKey: Uint8Array;
readonly #getPassword: GetPassword;
constructor({ networkId, accountIndex, encryptedRootPrivateKey, getPassword }: InMemoryKeyAgentProps) {
super();
this.#accountIndex = accountIndex;
this.#networkId = networkId;
this.#encryptedRootPrivateKey = encryptedRootPrivateKey;
this.#getPassword = getPassword;
}
get __typename(): KeyAgentType {
return KeyAgentType.InMemory;
}
get serializableData(): SerializableKeyAgentData {
return {
__typename: KeyAgentType.InMemory,
accountIndex: this.#accountIndex,
encryptedRootPrivateKeyBytes: [...this.#encryptedRootPrivateKey],
networkId: this.networkId
};
}
get networkId(): Cardano.NetworkId {
return this.#networkId;
}
get accountIndex(): number {
return this.#accountIndex;
}
async getExtendedAccountPublicKey(): Promise<Cardano.Bip32PublicKey> {
const privateKey = await this.#deriveAccountPrivateKey();
return Cardano.Bip32PublicKey(Buffer.from(privateKey.to_public().as_bytes()).toString('hex'));
}
async signBlob({ index, type }: AccountKeyDerivationPath, blob: HexBlob): Promise<SignBlobResult> {
const accountKey = await this.#deriveAccountPrivateKey();
const signingKey = accountKey.derive(type).derive(index).to_raw_key();
const signature = Cardano.Ed25519Signature(signingKey.sign(Buffer.from(blob, 'hex')).to_hex());
const publicKey = Cardano.Ed25519PublicKey(Buffer.from(signingKey.to_public().as_bytes()).toString('hex'));
return { publicKey, signature };
}
async derivePublicKey({ index, type }: AccountKeyDerivationPath): Promise<Cardano.Ed25519PublicKey> {
const accountPrivateKey = await this.#deriveAccountPrivateKey();
const cslPublicKey = accountPrivateKey.derive(type).derive(index).to_public().to_raw_key();
return Cardano.Ed25519PublicKey(Buffer.from(cslPublicKey.as_bytes()).toString('hex'));
}
// To export mnemonic, get entropy by reversing this:
// rootPrivateKey = CSL.Bip32PrivateKey.from_bip39_entropy(entropy, EMPTY_PASSWORD);
// eslint-disable-next-line max-len
// https://github.com/Emurgo/cardano-serialization-lib/blob/f817a033ade7a2255591d7c6444fa4f9ffbcf061/rust/src/chain_crypto/derive.rs#L30-L38
async exportRootPrivateKey(): Promise<Cardano.Bip32PrivateKey> {
const rootPrivateKey = await this.#decryptRootPrivateKey(true);
return Cardano.Bip32PrivateKey(Buffer.from(rootPrivateKey.as_bytes()).toString('hex'));
}
/**
* @throws AuthenticationError
*/
static async fromBip39MnemonicWords({
networkId,
getPassword,
mnemonicWords,
mnemonic2ndFactorPassphrase = Buffer.from(''),
accountIndex = 0
}: FromBip39MnemonicWordsProps): Promise<InMemoryKeyAgent> {
const mnemonic = joinMnemonicWords(mnemonicWords);
const validMnemonic = validateMnemonic(mnemonic);
if (!validMnemonic) throw new errors.InvalidMnemonicError();
const entropy = Buffer.from(mnemonicWordsToEntropy(mnemonicWords), 'hex');
const rootPrivateKey = CSL.Bip32PrivateKey.from_bip39_entropy(entropy, mnemonic2ndFactorPassphrase);
const password = await getPasswordRethrowTypedError(getPassword);
const encryptedRootPrivateKey = await emip3encrypt(rootPrivateKey.as_bytes(), password);
return new InMemoryKeyAgent({
accountIndex,
encryptedRootPrivateKey,
getPassword,
networkId
});
}
async #deriveAccountPrivateKey() {
const rootPrivateKey = await this.#decryptRootPrivateKey();
return rootPrivateKey.derive(harden(1852)).derive(harden(1815)).derive(harden(this.accountIndex));
}
async #decryptRootPrivateKey(noCache?: true) {
const password = await getPasswordRethrowTypedError(() => this.#getPassword(noCache));
let decryptedRootKeyBytes: Uint8Array;
try {
decryptedRootKeyBytes = await emip3decrypt(this.#encryptedRootPrivateKey, password);
} catch (error) {
throw new AuthenticationError('Failed to decrypt root private key', error);
}
return CSL.Bip32PrivateKey.from_bytes(decryptedRootKeyBytes);
}
}