@dfinity/hardware-wallet-cli 0.1.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.
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.LedgerIdentity = void 0;
26
+ const agent_1 = require("@dfinity/agent");
27
+ const principal_1 = require("@dfinity/principal");
28
+ const ledger_icp_1 = __importStar(require("@zondax/ledger-icp"));
29
+ const secp256k1_1 = require("./secp256k1");
30
+ // @ts-ignore (no types are available)
31
+ const hw_transport_webhid_1 = __importDefault(require("@ledgerhq/hw-transport-webhid"));
32
+ const hw_transport_node_hid_noevents_1 = __importDefault(require("@ledgerhq/hw-transport-node-hid-noevents"));
33
+ // Add polyfill for `window.fetch` for agent-js to work.
34
+ // @ts-ignore (no types are available)
35
+ const node_fetch_1 = __importDefault(require("node-fetch"));
36
+ global.fetch = node_fetch_1.default;
37
+ /**
38
+ * Convert the HttpAgentRequest body into cbor which can be signed by the Ledger Hardware Wallet.
39
+ * @param request - body of the HttpAgentRequest
40
+ */
41
+ function _prepareCborForLedger(request) {
42
+ return agent_1.Cbor.encode({ content: request });
43
+ }
44
+ /**
45
+ * A Hardware Ledger Internet Computer Agent identity.
46
+ */
47
+ class LedgerIdentity extends agent_1.SignIdentity {
48
+ constructor(derivePath, _publicKey) {
49
+ super();
50
+ this.derivePath = derivePath;
51
+ this._publicKey = _publicKey;
52
+ // A flag to signal that the next transaction to be signed will be
53
+ // a "stake neuron" transaction.
54
+ this._neuronStakeFlag = false;
55
+ }
56
+ /**
57
+ * Create a LedgerIdentity using the Web USB transport.
58
+ * @param derivePath The derivation path.
59
+ */
60
+ static async create(derivePath = `m/44'/223'/0'/0/0`) {
61
+ const [app, transport] = await this._connect();
62
+ try {
63
+ const publicKey = await this._fetchPublicKeyFromDevice(app, derivePath);
64
+ return new this(derivePath, publicKey);
65
+ }
66
+ finally {
67
+ // Always close the transport.
68
+ transport.close();
69
+ }
70
+ }
71
+ /**
72
+ * Connect to a ledger hardware wallet.
73
+ */
74
+ static async _connect() {
75
+ async function getTransport() {
76
+ if (await hw_transport_webhid_1.default.isSupported()) {
77
+ // We're in a web browser.
78
+ return hw_transport_webhid_1.default.create();
79
+ }
80
+ else if (await hw_transport_node_hid_noevents_1.default.isSupported()) {
81
+ // Maybe we're in a CLI.
82
+ return hw_transport_node_hid_noevents_1.default.create();
83
+ }
84
+ else {
85
+ // Unknown environment.
86
+ throw Error();
87
+ }
88
+ }
89
+ try {
90
+ const transport = await getTransport();
91
+ const app = new ledger_icp_1.default(transport);
92
+ return [app, transport];
93
+ }
94
+ catch (err) {
95
+ // @ts-ignore
96
+ if (err.id && err.id == "NoDeviceFound") {
97
+ throw "No Ledger device found. Is the wallet connected and unlocked?";
98
+ }
99
+ else if (
100
+ // @ts-ignore
101
+ err.message &&
102
+ // @ts-ignore
103
+ err.message.includes("cannot open device with path")) {
104
+ throw "Cannot connect to Ledger device. Please close all other wallet applications (e.g. Ledger Live) and try again.";
105
+ }
106
+ else {
107
+ // Unsupported browser. Data on browser compatibility is taken from https://caniuse.com/webhid
108
+ throw `Cannot connect to Ledger Wallet. Either you have other wallet applications open (e.g. Ledger Live), or your browser doesn't support WebHID, which is necessary to communicate with your Ledger hardware wallet.\n\nSupported browsers:\n* Chrome (Desktop) v89+\n* Edge v89+\n* Opera v76+\n\nError: ${err}`;
109
+ }
110
+ }
111
+ }
112
+ static async _fetchPublicKeyFromDevice(app, derivePath) {
113
+ const resp = await app.getAddressAndPubKey(derivePath);
114
+ // @ts-ignore
115
+ if (resp.returnCode == 28161) {
116
+ throw "Please open the Internet Computer app on your wallet and try again.";
117
+ }
118
+ else if (resp.returnCode == ledger_icp_1.LedgerError.TransactionRejected) {
119
+ throw "Ledger Wallet is locked. Unlock it and try again.";
120
+ // @ts-ignore
121
+ }
122
+ else if (resp.returnCode == 65535) {
123
+ throw "Unable to fetch the public key. Please try again.";
124
+ }
125
+ // This type doesn't have the right fields in it, so we have to manually type it.
126
+ const principal = resp
127
+ .principalText;
128
+ const publicKey = secp256k1_1.Secp256k1PublicKey.fromRaw(new Uint8Array(resp.publicKey));
129
+ if (principal !==
130
+ principal_1.Principal.selfAuthenticating(new Uint8Array(publicKey.toDer())).toText()) {
131
+ throw new Error("Principal returned by device does not match public key.");
132
+ }
133
+ return publicKey;
134
+ }
135
+ /**
136
+ * Required by Ledger.com that the user should be able to press a Button in UI
137
+ * and verify the address/pubkey are the same as on the device screen.
138
+ */
139
+ async showAddressAndPubKeyOnDevice() {
140
+ this._executeWithApp(async (app) => {
141
+ await app.showAddressAndPubKey(this.derivePath);
142
+ });
143
+ }
144
+ /**
145
+ * @returns The verion of the `Internet Computer' app installed on the Ledger device.
146
+ */
147
+ async getVersion() {
148
+ return this._executeWithApp(async (app) => {
149
+ const res = await app.getVersion();
150
+ return {
151
+ major: res.major,
152
+ minor: res.minor,
153
+ patch: res.patch,
154
+ };
155
+ });
156
+ }
157
+ getPublicKey() {
158
+ return this._publicKey;
159
+ }
160
+ async sign(blob) {
161
+ console.log("About to sign");
162
+ console.log(Buffer.from(blob).toString("hex"));
163
+ return await this._executeWithApp(async (app) => {
164
+ const resp = await app.sign(this.derivePath, Buffer.from(blob), this._neuronStakeFlag ? 1 : 0);
165
+ // Remove the "neuron stake" flag, since we already signed the transaction.
166
+ this._neuronStakeFlag = false;
167
+ const signatureRS = resp.signatureRS;
168
+ if (!signatureRS) {
169
+ throw new Error(`A ledger error happened during signature:\n` +
170
+ `Code: ${resp.returnCode}\n` +
171
+ `Message: ${JSON.stringify(resp.errorMessage)}\n`);
172
+ }
173
+ if (signatureRS?.byteLength !== 64) {
174
+ throw new Error(`Signature must be 64 bytes long (is ${signatureRS.length})`);
175
+ }
176
+ return bufferToArrayBuffer(signatureRS);
177
+ });
178
+ }
179
+ /**
180
+ * Signals that the upcoming transaction to be signed will be a "stake neuron" transaction.
181
+ */
182
+ flagUpcomingStakeNeuron() {
183
+ this._neuronStakeFlag = true;
184
+ }
185
+ async transformRequest(request) {
186
+ const { body, ...fields } = request;
187
+ const signature = await this.sign(_prepareCborForLedger(body));
188
+ return {
189
+ ...fields,
190
+ body: {
191
+ content: body,
192
+ sender_pubkey: this._publicKey.toDer(),
193
+ sender_sig: signature,
194
+ },
195
+ };
196
+ }
197
+ async _executeWithApp(func) {
198
+ const [app, transport] = await LedgerIdentity._connect();
199
+ try {
200
+ // Verify that the public key of the device matches the public key of this identity.
201
+ const devicePublicKey = await LedgerIdentity._fetchPublicKeyFromDevice(app, this.derivePath);
202
+ if (JSON.stringify(devicePublicKey) !== JSON.stringify(this._publicKey)) {
203
+ throw new Error("Found unexpected public key. Are you sure you're using the right wallet?");
204
+ }
205
+ // Run the provided function.
206
+ return await func(app);
207
+ }
208
+ finally {
209
+ transport.close();
210
+ }
211
+ }
212
+ }
213
+ exports.LedgerIdentity = LedgerIdentity;
214
+ function bufferToArrayBuffer(buffer) {
215
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
216
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Secp256k1PublicKey = void 0;
4
+ function equals(b1, b2) {
5
+ if (b1.byteLength !== b2.byteLength) {
6
+ return false;
7
+ }
8
+ const u1 = new Uint8Array(b1);
9
+ const u2 = new Uint8Array(b2);
10
+ for (let i = 0; i < u1.length; i++) {
11
+ if (u1[i] !== u2[i]) {
12
+ return false;
13
+ }
14
+ }
15
+ return true;
16
+ }
17
+ // This implementation is adjusted from the Ed25519PublicKey.
18
+ // The RAW_KEY_LENGTH and DER_PREFIX are modified accordingly
19
+ class Secp256k1PublicKey {
20
+ // `fromRaw` and `fromDer` should be used for instantiation, not this constructor.
21
+ constructor(key) {
22
+ this.rawKey = key;
23
+ this.derKey = Secp256k1PublicKey.derEncode(key);
24
+ }
25
+ static fromRaw(rawKey) {
26
+ return new Secp256k1PublicKey(rawKey);
27
+ }
28
+ static fromDer(derKey) {
29
+ return new Secp256k1PublicKey(this.derDecode(derKey));
30
+ }
31
+ static derEncode(publicKey) {
32
+ if (publicKey.byteLength !== Secp256k1PublicKey.RAW_KEY_LENGTH) {
33
+ const bl = publicKey.byteLength;
34
+ throw new TypeError(`secp256k1 public key must be ${Secp256k1PublicKey.RAW_KEY_LENGTH} bytes long (is ${bl})`);
35
+ }
36
+ const derPublicKey = Uint8Array.from([
37
+ ...Secp256k1PublicKey.DER_PREFIX,
38
+ ...new Uint8Array(publicKey),
39
+ ]);
40
+ return derPublicKey.buffer;
41
+ }
42
+ static derDecode(key) {
43
+ const expectedLength = Secp256k1PublicKey.DER_PREFIX.length + Secp256k1PublicKey.RAW_KEY_LENGTH;
44
+ if (key.byteLength !== expectedLength) {
45
+ const bl = key.byteLength;
46
+ throw new TypeError(`secp256k1 DER-encoded public key must be ${expectedLength} bytes long (is ${bl})`);
47
+ }
48
+ const rawKey = key.slice(0, Secp256k1PublicKey.DER_PREFIX.length);
49
+ if (!equals(this.derEncode(rawKey), key)) {
50
+ throw new TypeError("secp256k1 DER-encoded public key is invalid. A valid secp256k1 DER-encoded public key " +
51
+ `must have the following prefix: ${Secp256k1PublicKey.DER_PREFIX}`);
52
+ }
53
+ return rawKey;
54
+ }
55
+ toDer() {
56
+ return this.derKey;
57
+ }
58
+ toRaw() {
59
+ return this.rawKey;
60
+ }
61
+ }
62
+ exports.Secp256k1PublicKey = Secp256k1PublicKey;
63
+ // The length of secp256k1 public keys is always 65 bytes.
64
+ Secp256k1PublicKey.RAW_KEY_LENGTH = 65;
65
+ // Adding this prefix to a raw public key is sufficient to DER-encode it.
66
+ // prettier-ignore
67
+ Secp256k1PublicKey.DER_PREFIX = Uint8Array.from([
68
+ 0x30, 0x56,
69
+ 0x30, 0x10,
70
+ 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
71
+ 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a,
72
+ 0x03, 0x42,
73
+ 0x00, // no padding
74
+ ]);