@ledgerhq/hw-ledger-key-ring-protocol 0.2.1-nightly.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +33 -0
- package/.turbo/turbo-build.log +4 -0
- package/.unimportedrc.json +4 -0
- package/CHANGELOG.md +126 -0
- package/LICENSE.txt +21 -0
- package/README.md +3 -0
- package/jest.config.js +13 -0
- package/lib/ApduDevice.d.ts +99 -0
- package/lib/ApduDevice.d.ts.map +1 -0
- package/lib/ApduDevice.js +532 -0
- package/lib/ApduDevice.js.map +1 -0
- package/lib/BigEndian.d.ts +7 -0
- package/lib/BigEndian.d.ts.map +1 -0
- package/lib/BigEndian.js +26 -0
- package/lib/BigEndian.js.map +1 -0
- package/lib/CommandBlock.d.ts +114 -0
- package/lib/CommandBlock.d.ts.map +1 -0
- package/lib/CommandBlock.js +173 -0
- package/lib/CommandBlock.js.map +1 -0
- package/lib/CommandStream.d.ts +38 -0
- package/lib/CommandStream.d.ts.map +1 -0
- package/lib/CommandStream.js +197 -0
- package/lib/CommandStream.js.map +1 -0
- package/lib/CommandStreamDecoder.d.ts +15 -0
- package/lib/CommandStreamDecoder.d.ts.map +1 -0
- package/lib/CommandStreamDecoder.js +101 -0
- package/lib/CommandStreamDecoder.js.map +1 -0
- package/lib/CommandStreamEncoder.d.ts +16 -0
- package/lib/CommandStreamEncoder.d.ts.map +1 -0
- package/lib/CommandStreamEncoder.js +131 -0
- package/lib/CommandStreamEncoder.js.map +1 -0
- package/lib/CommandStreamJsonifier.d.ts +6 -0
- package/lib/CommandStreamJsonifier.d.ts.map +1 -0
- package/lib/CommandStreamJsonifier.js +75 -0
- package/lib/CommandStreamJsonifier.js.map +1 -0
- package/lib/CommandStreamResolver.d.ts +53 -0
- package/lib/CommandStreamResolver.d.ts.map +1 -0
- package/lib/CommandStreamResolver.js +221 -0
- package/lib/CommandStreamResolver.js.map +1 -0
- package/lib/Crypto.d.ts +38 -0
- package/lib/Crypto.d.ts.map +1 -0
- package/lib/Crypto.js +47 -0
- package/lib/Crypto.js.map +1 -0
- package/lib/Device.d.ts +43 -0
- package/lib/Device.d.ts.map +1 -0
- package/lib/Device.js +203 -0
- package/lib/Device.js.map +1 -0
- package/lib/IndexedTree.d.ts +13 -0
- package/lib/IndexedTree.d.ts.map +1 -0
- package/lib/IndexedTree.js +75 -0
- package/lib/IndexedTree.js.map +1 -0
- package/lib/NobleCrypto.d.ts +41 -0
- package/lib/NobleCrypto.d.ts.map +1 -0
- package/lib/NobleCrypto.js +298 -0
- package/lib/NobleCrypto.js.map +1 -0
- package/lib/PublicKey.d.ts +5 -0
- package/lib/PublicKey.d.ts.map +1 -0
- package/lib/PublicKey.js +10 -0
- package/lib/PublicKey.js.map +1 -0
- package/lib/SeedId.d.ts +80 -0
- package/lib/SeedId.d.ts.map +1 -0
- package/lib/SeedId.js +244 -0
- package/lib/SeedId.js.map +1 -0
- package/lib/StreamTree.d.ts +50 -0
- package/lib/StreamTree.d.ts.map +1 -0
- package/lib/StreamTree.js +169 -0
- package/lib/StreamTree.js.map +1 -0
- package/lib/StreamTreeCipher.d.ts +46 -0
- package/lib/StreamTreeCipher.d.ts.map +1 -0
- package/lib/StreamTreeCipher.js +179 -0
- package/lib/StreamTreeCipher.js.map +1 -0
- package/lib/__tests__/codec.d.ts +2 -0
- package/lib/__tests__/codec.d.ts.map +1 -0
- package/lib/__tests__/codec.js +108 -0
- package/lib/__tests__/codec.js.map +1 -0
- package/lib/__tests__/crypto.d.ts +2 -0
- package/lib/__tests__/crypto.d.ts.map +1 -0
- package/lib/__tests__/crypto.js +46 -0
- package/lib/__tests__/crypto.js.map +1 -0
- package/lib/__tests__/indexed_tree.d.ts +2 -0
- package/lib/__tests__/indexed_tree.d.ts.map +1 -0
- package/lib/__tests__/indexed_tree.js +45 -0
- package/lib/__tests__/indexed_tree.js.map +1 -0
- package/lib/__tests__/key_exchange.d.ts +2 -0
- package/lib/__tests__/key_exchange.d.ts.map +1 -0
- package/lib/__tests__/key_exchange.js +129 -0
- package/lib/__tests__/key_exchange.js.map +1 -0
- package/lib/__tests__/seedId.d.ts +2 -0
- package/lib/__tests__/seedId.d.ts.map +1 -0
- package/lib/__tests__/seedId.js +92 -0
- package/lib/__tests__/seedId.js.map +1 -0
- package/lib/__tests__/shared_object.d.ts +2 -0
- package/lib/__tests__/shared_object.d.ts.map +1 -0
- package/lib/__tests__/shared_object.js +78 -0
- package/lib/__tests__/shared_object.js.map +1 -0
- package/lib/index.d.ts +35 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +81 -0
- package/lib/index.js.map +1 -0
- package/lib/tlv.d.ts +99 -0
- package/lib/tlv.d.ts.map +1 -0
- package/lib/tlv.js +150 -0
- package/lib/tlv.js.map +1 -0
- package/lib-es/ApduDevice.d.ts +99 -0
- package/lib-es/ApduDevice.d.ts.map +1 -0
- package/lib-es/ApduDevice.js +526 -0
- package/lib-es/ApduDevice.js.map +1 -0
- package/lib-es/BigEndian.d.ts +7 -0
- package/lib-es/BigEndian.d.ts.map +1 -0
- package/lib-es/BigEndian.js +23 -0
- package/lib-es/BigEndian.js.map +1 -0
- package/lib-es/CommandBlock.d.ts +114 -0
- package/lib-es/CommandBlock.d.ts.map +1 -0
- package/lib-es/CommandBlock.js +160 -0
- package/lib-es/CommandBlock.js.map +1 -0
- package/lib-es/CommandStream.d.ts +38 -0
- package/lib-es/CommandStream.d.ts.map +1 -0
- package/lib-es/CommandStream.js +189 -0
- package/lib-es/CommandStream.js.map +1 -0
- package/lib-es/CommandStreamDecoder.d.ts +15 -0
- package/lib-es/CommandStreamDecoder.d.ts.map +1 -0
- package/lib-es/CommandStreamDecoder.js +97 -0
- package/lib-es/CommandStreamDecoder.js.map +1 -0
- package/lib-es/CommandStreamEncoder.d.ts +16 -0
- package/lib-es/CommandStreamEncoder.d.ts.map +1 -0
- package/lib-es/CommandStreamEncoder.js +127 -0
- package/lib-es/CommandStreamEncoder.js.map +1 -0
- package/lib-es/CommandStreamJsonifier.d.ts +6 -0
- package/lib-es/CommandStreamJsonifier.d.ts.map +1 -0
- package/lib-es/CommandStreamJsonifier.js +72 -0
- package/lib-es/CommandStreamJsonifier.js.map +1 -0
- package/lib-es/CommandStreamResolver.d.ts +53 -0
- package/lib-es/CommandStreamResolver.d.ts.map +1 -0
- package/lib-es/CommandStreamResolver.js +216 -0
- package/lib-es/CommandStreamResolver.js.map +1 -0
- package/lib-es/Crypto.d.ts +38 -0
- package/lib-es/Crypto.d.ts.map +1 -0
- package/lib-es/Crypto.js +43 -0
- package/lib-es/Crypto.js.map +1 -0
- package/lib-es/Device.d.ts +43 -0
- package/lib-es/Device.d.ts.map +1 -0
- package/lib-es/Device.js +195 -0
- package/lib-es/Device.js.map +1 -0
- package/lib-es/IndexedTree.d.ts +13 -0
- package/lib-es/IndexedTree.d.ts.map +1 -0
- package/lib-es/IndexedTree.js +71 -0
- package/lib-es/IndexedTree.js.map +1 -0
- package/lib-es/NobleCrypto.d.ts +41 -0
- package/lib-es/NobleCrypto.d.ts.map +1 -0
- package/lib-es/NobleCrypto.js +267 -0
- package/lib-es/NobleCrypto.js.map +1 -0
- package/lib-es/PublicKey.d.ts +5 -0
- package/lib-es/PublicKey.d.ts.map +1 -0
- package/lib-es/PublicKey.js +6 -0
- package/lib-es/PublicKey.js.map +1 -0
- package/lib-es/SeedId.d.ts +80 -0
- package/lib-es/SeedId.d.ts.map +1 -0
- package/lib-es/SeedId.js +235 -0
- package/lib-es/SeedId.js.map +1 -0
- package/lib-es/StreamTree.d.ts +50 -0
- package/lib-es/StreamTree.d.ts.map +1 -0
- package/lib-es/StreamTree.js +165 -0
- package/lib-es/StreamTree.js.map +1 -0
- package/lib-es/StreamTreeCipher.d.ts +46 -0
- package/lib-es/StreamTreeCipher.d.ts.map +1 -0
- package/lib-es/StreamTreeCipher.js +175 -0
- package/lib-es/StreamTreeCipher.js.map +1 -0
- package/lib-es/__tests__/codec.d.ts +2 -0
- package/lib-es/__tests__/codec.d.ts.map +1 -0
- package/lib-es/__tests__/codec.js +106 -0
- package/lib-es/__tests__/codec.js.map +1 -0
- package/lib-es/__tests__/crypto.d.ts +2 -0
- package/lib-es/__tests__/crypto.d.ts.map +1 -0
- package/lib-es/__tests__/crypto.js +44 -0
- package/lib-es/__tests__/crypto.js.map +1 -0
- package/lib-es/__tests__/indexed_tree.d.ts +2 -0
- package/lib-es/__tests__/indexed_tree.d.ts.map +1 -0
- package/lib-es/__tests__/indexed_tree.js +43 -0
- package/lib-es/__tests__/indexed_tree.js.map +1 -0
- package/lib-es/__tests__/key_exchange.d.ts +2 -0
- package/lib-es/__tests__/key_exchange.d.ts.map +1 -0
- package/lib-es/__tests__/key_exchange.js +124 -0
- package/lib-es/__tests__/key_exchange.js.map +1 -0
- package/lib-es/__tests__/seedId.d.ts +2 -0
- package/lib-es/__tests__/seedId.d.ts.map +1 -0
- package/lib-es/__tests__/seedId.js +90 -0
- package/lib-es/__tests__/seedId.js.map +1 -0
- package/lib-es/__tests__/shared_object.d.ts +2 -0
- package/lib-es/__tests__/shared_object.d.ts.map +1 -0
- package/lib-es/__tests__/shared_object.js +76 -0
- package/lib-es/__tests__/shared_object.js.map +1 -0
- package/lib-es/index.d.ts +35 -0
- package/lib-es/index.d.ts.map +1 -0
- package/lib-es/index.js +32 -0
- package/lib-es/index.js.map +1 -0
- package/lib-es/tlv.d.ts +99 -0
- package/lib-es/tlv.d.ts.map +1 -0
- package/lib-es/tlv.js +144 -0
- package/lib-es/tlv.js.map +1 -0
- package/package.json +63 -0
- package/src/ApduDevice.ts +692 -0
- package/src/BigEndian.ts +25 -0
- package/src/CommandBlock.ts +247 -0
- package/src/CommandStream.ts +262 -0
- package/src/CommandStreamDecoder.ts +142 -0
- package/src/CommandStreamEncoder.ts +144 -0
- package/src/CommandStreamJsonifier.ts +82 -0
- package/src/CommandStreamResolver.ts +284 -0
- package/src/Crypto.ts +78 -0
- package/src/Device.ts +254 -0
- package/src/IndexedTree.ts +80 -0
- package/src/NobleCrypto.ts +294 -0
- package/src/PublicKey.ts +6 -0
- package/src/SeedId.ts +338 -0
- package/src/StreamTree.ts +212 -0
- package/src/StreamTreeCipher.ts +207 -0
- package/src/__tests__/codec.ts +146 -0
- package/src/__tests__/crypto.ts +44 -0
- package/src/__tests__/indexed_tree.ts +51 -0
- package/src/__tests__/key_exchange.ts +167 -0
- package/src/__tests__/seedId.ts +120 -0
- package/src/__tests__/shared_object.ts +118 -0
- package/src/index.ts +43 -0
- package/src/tlv.ts +210 -0
- package/tsconfig.json +14 -0
package/src/Device.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { PublicKey } from "./PublicKey";
|
|
2
|
+
import {
|
|
3
|
+
CommandBlock,
|
|
4
|
+
CommandType,
|
|
5
|
+
signCommandBlock,
|
|
6
|
+
Derive,
|
|
7
|
+
PublishKey,
|
|
8
|
+
Seed,
|
|
9
|
+
} from "./CommandBlock";
|
|
10
|
+
import CommandStreamResolver from "./CommandStreamResolver";
|
|
11
|
+
import { crypto, DerivationPath, KeyPair } from "./Crypto";
|
|
12
|
+
import { StreamTree } from "./StreamTree";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
export interface Device {
|
|
18
|
+
// Get the public key of the device
|
|
19
|
+
getPublicKey(): Promise<PublicKey>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks wether the public key can be directly fetched or if acquiring the public key
|
|
23
|
+
* requires a user action.
|
|
24
|
+
*
|
|
25
|
+
* @returns True if the public key is directly available, false otherwise
|
|
26
|
+
*/
|
|
27
|
+
isPublicKeyAvailable(): boolean;
|
|
28
|
+
|
|
29
|
+
// The function receives the full stream but must only sign the last command block
|
|
30
|
+
sign(stream: CommandBlock[], tree?: StreamTree): Promise<CommandBlock>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read the symmetric key from the stream tree at the given path. This function may not be implemented by all devices.
|
|
34
|
+
* @param tree The stream tree
|
|
35
|
+
* @param path The path to the key
|
|
36
|
+
* @returns The public key of the symmetric key
|
|
37
|
+
*/
|
|
38
|
+
readKey(tree: StreamTree, path: number[]): Promise<Uint8Array>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SharedKey {
|
|
42
|
+
xpriv: Uint8Array;
|
|
43
|
+
publicKey: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface EncryptedSharedKey {
|
|
47
|
+
encryptedXpriv: Uint8Array;
|
|
48
|
+
publicKey: Uint8Array;
|
|
49
|
+
ephemeralPublicKey: Uint8Array;
|
|
50
|
+
initializationVector: Uint8Array;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class SoftwareDevice implements Device {
|
|
54
|
+
private keyPair: KeyPair;
|
|
55
|
+
|
|
56
|
+
constructor(kp: KeyPair) {
|
|
57
|
+
this.keyPair = kp;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getPublicKey(): Promise<PublicKey> {
|
|
61
|
+
return new PublicKey(this.keyPair.publicKey);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async generateSharedKey(): Promise<SharedKey> {
|
|
65
|
+
const xpriv = await crypto.randomBytes(64);
|
|
66
|
+
const pk = await crypto.derivePrivate(xpriv, []);
|
|
67
|
+
return { xpriv, publicKey: pk.publicKey };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async encryptSharedKey(
|
|
71
|
+
sharedKey: SharedKey,
|
|
72
|
+
recipient: Uint8Array,
|
|
73
|
+
): Promise<EncryptedSharedKey> {
|
|
74
|
+
const kp = await crypto.randomKeypair();
|
|
75
|
+
const ecdh = await crypto.ecdh(kp, recipient);
|
|
76
|
+
const initializationVector = await crypto.randomBytes(16);
|
|
77
|
+
const encryptedXpriv = await crypto.encrypt(ecdh, initializationVector, sharedKey.xpriv);
|
|
78
|
+
return {
|
|
79
|
+
encryptedXpriv,
|
|
80
|
+
publicKey: sharedKey.publicKey,
|
|
81
|
+
ephemeralPublicKey: kp.publicKey,
|
|
82
|
+
initializationVector,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async decryptSharedKey(encryptedSharedKey: EncryptedSharedKey): Promise<SharedKey> {
|
|
87
|
+
const ecdh = await crypto.ecdh(this.keyPair, encryptedSharedKey.ephemeralPublicKey);
|
|
88
|
+
const xpriv = await crypto.decrypt(
|
|
89
|
+
ecdh,
|
|
90
|
+
encryptedSharedKey.initializationVector,
|
|
91
|
+
encryptedSharedKey.encryptedXpriv,
|
|
92
|
+
);
|
|
93
|
+
return { xpriv, publicKey: encryptedSharedKey.publicKey };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async deriveKey(tree: StreamTree, path: number[]): Promise<SharedKey> {
|
|
97
|
+
const event = await tree.getPublishKeyEvent(this.keyPair.publicKey, path);
|
|
98
|
+
if (!event) {
|
|
99
|
+
throw new Error("Cannot find key in the tree for the current device");
|
|
100
|
+
}
|
|
101
|
+
const encryptedSharedKey = {
|
|
102
|
+
encryptedXpriv: event.encryptedXpriv,
|
|
103
|
+
publicKey: event.groupPublicKey,
|
|
104
|
+
ephemeralPublicKey: event.ephemeralPublicKey,
|
|
105
|
+
initializationVector: event.nonce,
|
|
106
|
+
};
|
|
107
|
+
const sharedKey = await this.decryptSharedKey(encryptedSharedKey);
|
|
108
|
+
const newKey = await crypto.derivePrivate(sharedKey.xpriv, path);
|
|
109
|
+
const xpriv = new Uint8Array(64);
|
|
110
|
+
xpriv.set(newKey.privateKey);
|
|
111
|
+
xpriv.set(newKey.chainCode, 32);
|
|
112
|
+
return { xpriv, publicKey: newKey.publicKey };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async sign(stream: CommandBlock[], tree?: StreamTree): Promise<CommandBlock> {
|
|
116
|
+
if (stream.length === 0) {
|
|
117
|
+
throw new Error("Cannot sign an empty stream");
|
|
118
|
+
}
|
|
119
|
+
if (stream[stream.length - 1].commands.length === 0) {
|
|
120
|
+
throw new Error("Cannot sign an empty block");
|
|
121
|
+
}
|
|
122
|
+
const lastBlock = stream[stream.length - 1];
|
|
123
|
+
|
|
124
|
+
lastBlock.issuer = this.keyPair.publicKey;
|
|
125
|
+
|
|
126
|
+
// Resolve the stream (before the last block)
|
|
127
|
+
const resolved = await CommandStreamResolver.resolve(stream.slice(0, stream.length - 1));
|
|
128
|
+
|
|
129
|
+
// The shared key of the stream
|
|
130
|
+
|
|
131
|
+
let sharedKey: SharedKey | null = null;
|
|
132
|
+
|
|
133
|
+
// Iterate through the commands to inject encrypted keys
|
|
134
|
+
for (let commandIndex = 0; commandIndex < lastBlock.commands.length; commandIndex++) {
|
|
135
|
+
const command = lastBlock.commands[commandIndex];
|
|
136
|
+
switch (command.getType()) {
|
|
137
|
+
case CommandType.Seed: {
|
|
138
|
+
// Generate the shared key
|
|
139
|
+
sharedKey = await this.generateSharedKey();
|
|
140
|
+
|
|
141
|
+
// Encrypt the shared key and inject it in the command
|
|
142
|
+
const encryptedSharedKey = await this.encryptSharedKey(sharedKey, this.keyPair.publicKey);
|
|
143
|
+
(command as Seed).groupKey = sharedKey.publicKey;
|
|
144
|
+
(command as Seed).encryptedXpriv = encryptedSharedKey.encryptedXpriv;
|
|
145
|
+
(command as Seed).ephemeralPublicKey = encryptedSharedKey.ephemeralPublicKey;
|
|
146
|
+
(command as Seed).initializationVector = encryptedSharedKey.initializationVector;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case CommandType.Derive: {
|
|
150
|
+
// Derive the shared key from the tree
|
|
151
|
+
if (!tree) {
|
|
152
|
+
throw new Error("Cannot derive a key without a tree");
|
|
153
|
+
}
|
|
154
|
+
sharedKey = await this.deriveKey(tree, (command as Derive).path);
|
|
155
|
+
|
|
156
|
+
// Encrypt the shared key and inject it in the command
|
|
157
|
+
const encryptedDerivedKey = await this.encryptSharedKey(
|
|
158
|
+
sharedKey,
|
|
159
|
+
this.keyPair.publicKey,
|
|
160
|
+
);
|
|
161
|
+
(command as Derive).groupKey = sharedKey.publicKey;
|
|
162
|
+
(command as Derive).encryptedXpriv = encryptedDerivedKey.encryptedXpriv;
|
|
163
|
+
(command as Derive).initializationVector = encryptedDerivedKey.initializationVector;
|
|
164
|
+
(command as Derive).ephemeralPublicKey = encryptedDerivedKey.ephemeralPublicKey;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case CommandType.PublishKey: {
|
|
168
|
+
// Derive the shared key from the tree
|
|
169
|
+
if (!sharedKey) {
|
|
170
|
+
// If the current stream is the seed stream, read the key from the first command in the first block
|
|
171
|
+
const encryptedKey = resolved.getEncryptedKey(this.keyPair.publicKey);
|
|
172
|
+
if (encryptedKey) {
|
|
173
|
+
sharedKey = await this.decryptSharedKey({
|
|
174
|
+
encryptedXpriv: encryptedKey.encryptedXpriv,
|
|
175
|
+
initializationVector: encryptedKey.initialiationVector,
|
|
176
|
+
publicKey: encryptedKey.issuer,
|
|
177
|
+
ephemeralPublicKey: encryptedKey.ephemeralPublicKey,
|
|
178
|
+
});
|
|
179
|
+
} else if (stream[0].commands[0].getType() == CommandType.Seed) {
|
|
180
|
+
if (crypto.to_hex(stream[0].issuer) !== crypto.to_hex(this.keyPair.publicKey)) {
|
|
181
|
+
throw new Error("Cannot read the seed key from another device");
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// console.dir(stream, { depth: null });
|
|
185
|
+
sharedKey = await this.deriveKey(tree!, resolved.getStreamDerivationPath());
|
|
186
|
+
}
|
|
187
|
+
if (!sharedKey) throw new Error("Cannot find the shared key");
|
|
188
|
+
}
|
|
189
|
+
const encryptedSharedKey = await this.encryptSharedKey(
|
|
190
|
+
sharedKey!,
|
|
191
|
+
(command as PublishKey).recipient,
|
|
192
|
+
);
|
|
193
|
+
(command as PublishKey).encryptedXpriv = encryptedSharedKey.encryptedXpriv;
|
|
194
|
+
(command as PublishKey).initializationVector = encryptedSharedKey.initializationVector;
|
|
195
|
+
(command as PublishKey).ephemeralPublicKey = encryptedSharedKey.ephemeralPublicKey;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const signature = (
|
|
201
|
+
await signCommandBlock(
|
|
202
|
+
lastBlock,
|
|
203
|
+
(await this.getPublicKey()).publicKey,
|
|
204
|
+
this.keyPair.privateKey,
|
|
205
|
+
)
|
|
206
|
+
).signature;
|
|
207
|
+
lastBlock.signature = signature;
|
|
208
|
+
return lastBlock;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async readKey(tree: StreamTree, path: number[]): Promise<Uint8Array> {
|
|
212
|
+
const event = await tree.getPublishKeyEvent(this.keyPair.publicKey, path);
|
|
213
|
+
if (!event) {
|
|
214
|
+
throw new Error("Cannot find key in the tree for the current device");
|
|
215
|
+
}
|
|
216
|
+
const encryptedSharedKey: EncryptedSharedKey = {
|
|
217
|
+
encryptedXpriv: event.encryptedXpriv,
|
|
218
|
+
initializationVector: event.nonce,
|
|
219
|
+
publicKey: event.groupPublicKey,
|
|
220
|
+
ephemeralPublicKey: event.ephemeralPublicKey,
|
|
221
|
+
};
|
|
222
|
+
const sharedKey = await this.decryptSharedKey(encryptedSharedKey);
|
|
223
|
+
|
|
224
|
+
// Derive the key to match the path
|
|
225
|
+
let index = DerivationPath.toIndexArray(event.stream.getStreamPath()!).length;
|
|
226
|
+
while (index < path.length) {
|
|
227
|
+
const derivation = await crypto.derivePrivate(sharedKey.xpriv, [index]);
|
|
228
|
+
const xpriv = new Uint8Array(64);
|
|
229
|
+
xpriv.set(derivation.privateKey);
|
|
230
|
+
xpriv.set(derivation.chainCode, 32);
|
|
231
|
+
sharedKey.xpriv = xpriv;
|
|
232
|
+
sharedKey.publicKey = derivation.publicKey;
|
|
233
|
+
index += 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return sharedKey.xpriv;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
isPublicKeyAvailable(): boolean {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
*
|
|
246
|
+
*/
|
|
247
|
+
export async function createDevice(): Promise<Device> {
|
|
248
|
+
const kp = await crypto.randomKeypair();
|
|
249
|
+
return new SoftwareDevice(kp);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const ISSUER_PLACEHOLDER = new Uint8Array([
|
|
253
|
+
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
254
|
+
]);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export class IndexedTree<T> {
|
|
2
|
+
private node: T | null;
|
|
3
|
+
private children: Map<number, IndexedTree<T>>;
|
|
4
|
+
|
|
5
|
+
constructor(node: T | null, children: Map<number, IndexedTree<T>> = new Map()) {
|
|
6
|
+
this.node = node;
|
|
7
|
+
this.children = children;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public getHighestIndex(): number {
|
|
11
|
+
return [...this.children.keys()].reduce((a, b) => Math.max(a, b), 0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public getChildren(): Map<number, IndexedTree<T>> {
|
|
15
|
+
return this.children;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public getChild(index: number): IndexedTree<T> | undefined {
|
|
19
|
+
return this.children.get(index);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public findChild(path: number[]): IndexedTree<T> | undefined {
|
|
23
|
+
if (path.length === 0) {
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
const index = path[0];
|
|
27
|
+
const rest = path.slice(1);
|
|
28
|
+
if (this.children.has(index)) {
|
|
29
|
+
return this.children.get(index)!.findChild(rest);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public getValue(): T | null {
|
|
35
|
+
return this.node;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Update the value of the node, if the node doesn't exist, it will be created
|
|
39
|
+
public updateChild(path: number[], value: T): IndexedTree<T> {
|
|
40
|
+
if (path.length === 0) {
|
|
41
|
+
return new IndexedTree(value, this.children);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const index = path[0];
|
|
45
|
+
const rest = path.slice(1);
|
|
46
|
+
const children = new Map(this.children);
|
|
47
|
+
if (this.children.has(index)) {
|
|
48
|
+
const subTree = this.children.get(index)!.updateChild(rest, value);
|
|
49
|
+
children.set(index, subTree);
|
|
50
|
+
} else {
|
|
51
|
+
const subTree = new IndexedTree<T>(null).updateChild(rest, value);
|
|
52
|
+
children.set(index, subTree);
|
|
53
|
+
}
|
|
54
|
+
return new IndexedTree(this.node, children);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Adds a subtree to the tree
|
|
58
|
+
public addChild(path: number[], child: IndexedTree<T>): IndexedTree<T> {
|
|
59
|
+
if (path.length === 0) {
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
if (path.length == 1) {
|
|
63
|
+
const children = new Map(this.children);
|
|
64
|
+
children.set(path[0], child);
|
|
65
|
+
return new IndexedTree(this.node, children);
|
|
66
|
+
}
|
|
67
|
+
const index = path[0];
|
|
68
|
+
const rest = path.slice(1);
|
|
69
|
+
const children = new Map(this.children);
|
|
70
|
+
|
|
71
|
+
if (this.children.has(index)) {
|
|
72
|
+
const subTree = this.children.get(index)!.addChild(rest, child);
|
|
73
|
+
children.set(index, subTree);
|
|
74
|
+
} else {
|
|
75
|
+
const subTree = new IndexedTree<T>(null).addChild(rest, child);
|
|
76
|
+
children.set(index, subTree);
|
|
77
|
+
}
|
|
78
|
+
return new IndexedTree(this.node, children);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import * as secp256k1 from "secp256k1";
|
|
2
|
+
import * as ecc from "tiny-secp256k1";
|
|
3
|
+
import { BIP32Factory } from "bip32";
|
|
4
|
+
import hmac from "create-hmac";
|
|
5
|
+
import * as crypto from "crypto";
|
|
6
|
+
|
|
7
|
+
import { Crypto, KeyPair, KeyPairWithChainCode } from "./Crypto";
|
|
8
|
+
|
|
9
|
+
const bip32 = BIP32Factory(ecc);
|
|
10
|
+
const AES_BLOCK_SIZE = 16;
|
|
11
|
+
const PRIVATE_KEY_SIZE = 32;
|
|
12
|
+
|
|
13
|
+
export class NobleCryptoSecp256k1 implements Crypto {
|
|
14
|
+
async randomKeypair(): Promise<KeyPair> {
|
|
15
|
+
let pk: Uint8Array;
|
|
16
|
+
do {
|
|
17
|
+
pk = crypto.randomBytes(PRIVATE_KEY_SIZE);
|
|
18
|
+
} while (!secp256k1.privateKeyVerify(pk));
|
|
19
|
+
return this.keypairFromSecretKey(pk);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async derivePrivate(xpriv: Uint8Array, path: number[]): Promise<KeyPairWithChainCode> {
|
|
23
|
+
const pk = xpriv.slice(0, 32);
|
|
24
|
+
const chainCode = xpriv.slice(32);
|
|
25
|
+
let node = bip32.fromPrivateKey(Buffer.from(pk), Buffer.from(chainCode));
|
|
26
|
+
for (const index of path) {
|
|
27
|
+
node = node.derive(index);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
publicKey: this.to_array(node.publicKey),
|
|
31
|
+
privateKey: this.to_array(node.privateKey!),
|
|
32
|
+
chainCode: this.to_array(node.chainCode),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async keypairFromSecretKey(secretKey: Uint8Array): Promise<KeyPair> {
|
|
37
|
+
return {
|
|
38
|
+
publicKey: secp256k1.publicKeyCreate(secretKey),
|
|
39
|
+
privateKey: secretKey,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private derEncode(R: Uint8Array, S: Uint8Array): Uint8Array {
|
|
44
|
+
if (R[0] > 0x7f) {
|
|
45
|
+
R = this.concat(new Uint8Array([0x00]), R);
|
|
46
|
+
}
|
|
47
|
+
if (S[0] > 0x7f) {
|
|
48
|
+
S = this.concat(new Uint8Array([0x00]), S);
|
|
49
|
+
}
|
|
50
|
+
R = this.concat(new Uint8Array([0x02, R.length]), R);
|
|
51
|
+
S = this.concat(new Uint8Array([0x02, S.length]), S);
|
|
52
|
+
const prefix = new Uint8Array([0x30, R.length + S.length]);
|
|
53
|
+
return this.concat(prefix, this.concat(R, S));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private derDecode(signature: Uint8Array): { R: Uint8Array; S: Uint8Array } {
|
|
57
|
+
const R: Uint8Array = signature.slice(4, 4 + signature[3]);
|
|
58
|
+
const S: Uint8Array = signature.slice(
|
|
59
|
+
6 + signature[3],
|
|
60
|
+
6 + signature[3] + signature[5 + signature[3]],
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
R: this.enforceLength(R, PRIVATE_KEY_SIZE),
|
|
64
|
+
S: this.enforceLength(S, PRIVATE_KEY_SIZE),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async sign(message: Uint8Array, keyPair: KeyPair): Promise<Uint8Array> {
|
|
69
|
+
const signature = secp256k1.ecdsaSign(message, keyPair.privateKey).signature;
|
|
70
|
+
// DER encoding
|
|
71
|
+
return this.derEncode(signature.slice(0, 32), signature.slice(32, 64));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async verify(
|
|
75
|
+
message: Uint8Array,
|
|
76
|
+
signature: Uint8Array,
|
|
77
|
+
publicKey: Uint8Array,
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
// DER decoding
|
|
80
|
+
const { R, S } = this.derDecode(signature);
|
|
81
|
+
return secp256k1.ecdsaVerify(this.concat(R, S), message, publicKey);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private to_array(buffer: Buffer): Uint8Array {
|
|
85
|
+
return new Uint8Array(buffer);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private normalizeKey(key: Uint8Array): Uint8Array {
|
|
89
|
+
if (key.length === 32) {
|
|
90
|
+
return key;
|
|
91
|
+
}
|
|
92
|
+
throw new Error("Invalid key length for AES-256 " + `(invalid length is ${key.length})`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private normalizeNonce(nonce: Uint8Array): Uint8Array {
|
|
96
|
+
if (nonce.length < 16) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Invalid nonce length (must be 128bits) " + `(invalid length is ${nonce.length})`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return nonce.slice(0, 16);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private concat(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
105
|
+
const c = new Uint8Array(a.length + b.length);
|
|
106
|
+
c.set(a);
|
|
107
|
+
c.set(b, a.length);
|
|
108
|
+
return c;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private enforceLength(buffer: Uint8Array, length: number): Uint8Array {
|
|
112
|
+
if (buffer.length > length) {
|
|
113
|
+
return buffer.slice(buffer.length - length); // truncate extra bytes from the start
|
|
114
|
+
} else if (buffer.length < length) {
|
|
115
|
+
const padded = new Uint8Array(length);
|
|
116
|
+
const start = length - buffer.length;
|
|
117
|
+
padded.set(Array(start).fill(0));
|
|
118
|
+
padded.set(buffer, start);
|
|
119
|
+
return padded;
|
|
120
|
+
}
|
|
121
|
+
return buffer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private pad(message: Uint8Array): Uint8Array {
|
|
125
|
+
// ISO9797M2 implementation
|
|
126
|
+
const padLength = AES_BLOCK_SIZE - (message.length % AES_BLOCK_SIZE);
|
|
127
|
+
if (padLength === AES_BLOCK_SIZE) {
|
|
128
|
+
return message;
|
|
129
|
+
}
|
|
130
|
+
const padding = new Uint8Array(padLength);
|
|
131
|
+
padding[0] = 0x80;
|
|
132
|
+
padding.fill(0, 1);
|
|
133
|
+
return this.concat(message, padding);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private unpad(message: Uint8Array): Uint8Array {
|
|
137
|
+
// ISO9797M2 implementation
|
|
138
|
+
for (let i = message.length - 1; i >= 0; i--) {
|
|
139
|
+
if (message[i] === 0x80) {
|
|
140
|
+
return message.slice(0, i);
|
|
141
|
+
}
|
|
142
|
+
if (message[i] !== 0x00) {
|
|
143
|
+
return message;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw new Error("Invalid padding");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async encrypt(secret: Uint8Array, nonce: Uint8Array, message: Uint8Array): Promise<Uint8Array> {
|
|
150
|
+
const normalizedSecret = this.normalizeKey(secret);
|
|
151
|
+
const normalizeNonce = this.normalizeNonce(nonce);
|
|
152
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", normalizedSecret, normalizeNonce);
|
|
153
|
+
cipher.setAutoPadding(false);
|
|
154
|
+
let result = cipher.update(this.to_hex(message), "hex", "hex");
|
|
155
|
+
result += cipher.final("hex");
|
|
156
|
+
const bytes = this.from_hex(result);
|
|
157
|
+
return this.concat(bytes, cipher.getAuthTag());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async decrypt(
|
|
161
|
+
secret: Uint8Array,
|
|
162
|
+
nonce: Uint8Array,
|
|
163
|
+
ciphertext: Uint8Array,
|
|
164
|
+
): Promise<Uint8Array> {
|
|
165
|
+
const normalizedSecret = this.normalizeKey(secret);
|
|
166
|
+
const normalizeNonce = this.normalizeNonce(nonce);
|
|
167
|
+
const encryptedData = ciphertext.slice(0, ciphertext.length - AES_BLOCK_SIZE);
|
|
168
|
+
const authTag = ciphertext.slice(encryptedData.length);
|
|
169
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", normalizedSecret, normalizeNonce);
|
|
170
|
+
decipher.setAuthTag(authTag);
|
|
171
|
+
let result = decipher.update(this.to_hex(encryptedData), "hex", "hex");
|
|
172
|
+
result += decipher.final("hex");
|
|
173
|
+
return this.from_hex(result);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Ledger Live data are encrypted following pattern based on ECIES.
|
|
178
|
+
* For each encryption the Ledger Live instance generates a random keypair over secp256k1 (ephemeral public key)
|
|
179
|
+
* and a 16 bytes IV. Ledger Live then perform an ECDH between the command stream public key and
|
|
180
|
+
* the ephemeral private key to get the encryption key.
|
|
181
|
+
* The data is then encrypted using AES-256-GCM and serialized using the following format:
|
|
182
|
+
1 byte : Version of the format (0x00)
|
|
183
|
+
33 bytes : Compressed ephemeral public key
|
|
184
|
+
16 bytes : Nonce/IV
|
|
185
|
+
16 bytes : Tag/MAC (from AES-256-GCM)
|
|
186
|
+
variable : Encrypted data
|
|
187
|
+
*/
|
|
188
|
+
async encryptUserData(
|
|
189
|
+
commandStreamPrivateKey: Uint8Array,
|
|
190
|
+
data: Uint8Array,
|
|
191
|
+
): Promise<Uint8Array> {
|
|
192
|
+
// Generate ephemeral key pair
|
|
193
|
+
const ephemeralKeypair = await this.randomKeypair();
|
|
194
|
+
|
|
195
|
+
// Derive the shared secret using ECDH
|
|
196
|
+
const sharedSecret = await this.ecdh(
|
|
197
|
+
await this.keypairFromSecretKey(commandStreamPrivateKey),
|
|
198
|
+
ephemeralKeypair.publicKey,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Normalize the shared secret to be used as AES key
|
|
202
|
+
const aesKey = await this.computeSymmetricKey(sharedSecret, new Uint8Array());
|
|
203
|
+
|
|
204
|
+
// Generate a random IV (nonce)
|
|
205
|
+
const iv = crypto.randomBytes(16);
|
|
206
|
+
|
|
207
|
+
// Encrypt the data using AES-256-GCM
|
|
208
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
209
|
+
let encryptedData = cipher.update(data);
|
|
210
|
+
encryptedData = Buffer.concat([encryptedData, cipher.final()]);
|
|
211
|
+
const tag = cipher.getAuthTag();
|
|
212
|
+
|
|
213
|
+
// Serialize the format
|
|
214
|
+
const result = new Uint8Array(
|
|
215
|
+
1 + ephemeralKeypair.publicKey.length + iv.length + tag.length + encryptedData.length,
|
|
216
|
+
);
|
|
217
|
+
result[0] = 0x00; // Version of the format
|
|
218
|
+
result.set(ephemeralKeypair.publicKey, 1);
|
|
219
|
+
result.set(iv, 34);
|
|
220
|
+
result.set(tag, 50);
|
|
221
|
+
result.set(encryptedData, 66);
|
|
222
|
+
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async decryptUserData(
|
|
227
|
+
commandStreamPrivateKey: Uint8Array,
|
|
228
|
+
data: Uint8Array,
|
|
229
|
+
): Promise<Uint8Array> {
|
|
230
|
+
const version = data[0];
|
|
231
|
+
if (version !== 0x00) {
|
|
232
|
+
throw new Error("Unsupported format version");
|
|
233
|
+
}
|
|
234
|
+
const ephemeralPublicKey = data.slice(1, 34);
|
|
235
|
+
const iv = data.slice(34, 50);
|
|
236
|
+
const tag = data.slice(50, 66);
|
|
237
|
+
const encryptedData = data.slice(66);
|
|
238
|
+
|
|
239
|
+
// Derive the shared secret using ECDH
|
|
240
|
+
const sharedSecret = await this.ecdh(
|
|
241
|
+
await this.keypairFromSecretKey(commandStreamPrivateKey),
|
|
242
|
+
ephemeralPublicKey,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Normalize the shared secret to be used as AES key
|
|
246
|
+
const aesKey = await this.computeSymmetricKey(sharedSecret, new Uint8Array());
|
|
247
|
+
|
|
248
|
+
// Decrypt the data using AES-256-GCM
|
|
249
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
250
|
+
decipher.setAuthTag(tag);
|
|
251
|
+
let decryptedData = decipher.update(encryptedData);
|
|
252
|
+
decryptedData = Buffer.concat([decryptedData, decipher.final()]);
|
|
253
|
+
return new Uint8Array(decryptedData.buffer, decryptedData.byteOffset, decryptedData.byteLength);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async randomBytes(size: number): Promise<Uint8Array> {
|
|
257
|
+
return crypto.randomBytes(size);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async ecdh(keyPair: KeyPair, publicKey: Uint8Array): Promise<Uint8Array> {
|
|
261
|
+
const pubkey = Buffer.from(publicKey);
|
|
262
|
+
const privkey = Buffer.from(keyPair.privateKey);
|
|
263
|
+
const point = ecc.pointMultiply(pubkey, privkey, ecc.isPointCompressed(pubkey))!;
|
|
264
|
+
return point.slice(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async computeSymmetricKey(privateKey: Uint8Array, extra: Uint8Array): Promise<Uint8Array> {
|
|
268
|
+
const digest = hmac("sha256", Buffer.from(extra)).update(Buffer.from(privateKey)).digest();
|
|
269
|
+
return digest;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async hash(message: Uint8Array): Promise<Uint8Array> {
|
|
273
|
+
return crypto.createHash("sha256").update(Buffer.from(message)).digest();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
from_hex(hex: string): Uint8Array {
|
|
277
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
278
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
279
|
+
bytes[i / 2] = parseInt(hex[i] + hex[i + 1], 16);
|
|
280
|
+
}
|
|
281
|
+
return bytes;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
to_hex(bytes?: Uint8Array | undefined | null): string {
|
|
285
|
+
return to_hex(bytes);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function to_hex(bytes?: Uint8Array | undefined | null): string {
|
|
290
|
+
if (!bytes) {
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
|
294
|
+
}
|