Skip to content

Commit

Permalink
feat(connect): resetDevice with entropy check
Browse files Browse the repository at this point in the history
  • Loading branch information
szymonlesisz committed Dec 11, 2024
1 parent fef3825 commit f54ba20
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 20 deletions.
2 changes: 2 additions & 0 deletions packages/connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"@ethereumjs/common": "^4.4.0",
"@ethereumjs/tx": "^5.4.0",
"@fivebinaries/coin-selection": "2.2.1",
"@noble/hashes": "^1.6.1",
"@trezor/blockchain-link": "workspace:*",
"@trezor/blockchain-link-types": "workspace:*",
"@trezor/connect-analytics": "workspace:*",
Expand All @@ -82,6 +83,7 @@
"@trezor/transport": "workspace:*",
"@trezor/utils": "workspace:*",
"@trezor/utxo-lib": "workspace:*",
"bip39": "^3.1.0",
"blakejs": "^1.2.1",
"bs58": "^6.0.0",
"bs58check": "^4.0.0",
Expand Down
35 changes: 35 additions & 0 deletions packages/connect/src/api/firmware/__tests__/verifyEntropy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { verifyEntropy } from '../verifyEntropy';

describe('firmware/verifyEntropy', () => {
it('bip39 success', async () => {
const response = await verifyEntropy({
type: undefined,
strength: 256,
hostEntropy: '16180a5ae8e1b3976367cb9afa79856b4e7fea76c8751d37debc0b89f4942a73',
trezorEntropy: '3b3bc4625e77c23825770c4240be26319f1c74bf72c14204293208f174d5d6a3',
xpubs: {
"m/84'/0'/0'":
'xpub6CnUiQ2hMiKiwjYZ467qtSQVNmUertqUDYTdSLkZzTr6Y7b66WhCiPeA1RXTfC1ZWgqsiqH1uY7tgGW7xPEN1361vw7QsEr9zAiibayh7rg',
"m/44'/60'/0'":
'xpub6DLfqKZGZAzXAdEL8dA6u84An3mh1QuSHaxhhvswB9BKzfzEUes31pZZ1LzV7e8iDRfKhwo2xTwoizqSBaHLZJbHbKnHAseZJneLefbXwce',
},
});
expect(response.success).toEqual(true);
});

it('slip39 success', async () => {
const response = await verifyEntropy({
type: 1,
strength: 256,
hostEntropy: '0f675453428a5c0075f75a43bf0bacc6b46053d85ac96a1923c52f8c8a73cfb3',
trezorEntropy: '3ca3d7ec6b25481fce7b7439d550cba730ecc3a8cf112defe2540814d94ebfdc',
xpubs: {
"m/84'/0'/0'":
'xpub6CToV2Azz3zvtb9J25ge7oPux9SWHk61DHk3Y9H4wXpBwKTd7BHYEPMTm6SvmiNaZDecfk5qN1mtXPx5kJwy7kyYTqhEUpJU4NFSpXwm4fR',
"m/44'/60'/0'":
'xpub6CHPgi7CWuYVXyB98q9cW6qPbxxAB9WWTZqCofsSKgasrapwDDWeUxx9D3p3r6UKBdVCndP7AFjj6QG7tdTSaunPo1DFETSaHiKe6Ds6tsr',
},
});
expect(response.success).toEqual(true);
});
});
129 changes: 129 additions & 0 deletions packages/connect/src/api/firmware/verifyEntropy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { entropyToMnemonic, mnemonicToSeed } from 'bip39';
import { pbkdf2 } from '@noble/hashes/pbkdf2';
import { sha256 } from '@noble/hashes/sha256';
import { randomBytes } from '@noble/hashes/utils';

import { bip32 } from '@trezor/utxo-lib';

import { PROTO } from '../../constants';

export const generateEntropy = (len: number) => {
try {
return randomBytes(len);
} catch {
throw new Error('generateEntropy: Environment does not support crypto random');
}
};

// https://github.com/trezor/python-shamir-mnemonic/blob/master/shamir_mnemonic/cipher.py
const BASE_ITERATION_COUNT = 10000;
const ROUND_COUNT = 4;

// https://github.com/trezor/python-shamir-mnemonic/blob/master/shamir_mnemonic/cipher.py
const roundFunction = (i: number, passphrase: Buffer, e: number, salt: Buffer, r: Buffer) => {
const data = Buffer.concat([Buffer.from([i]), passphrase]);
const iterations = Math.floor((BASE_ITERATION_COUNT << e) / ROUND_COUNT);

const result = pbkdf2(sha256, data, Buffer.concat([salt, r]), {
c: iterations,
dkLen: r.length,
});

return Buffer.from(result);
};

// https://github.com/trezor/python-shamir-mnemonic/blob/master/shamir_mnemonic/cipher.py
const xor = (a: Buffer, b: Buffer) => {
if (a.length !== b.length) {
throw new Error('Buffers must be of equal length to XOR.');
}
const result = Buffer.alloc(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = a[i] ^ b[i];
}

return result;
};

// https://github.com/trezor/python-shamir-mnemonic/blob/master/shamir_mnemonic/cipher.py
// simplified "decrypt" function
const entropyToSeedSlip39 = (encryptedSecret: Buffer) => {
const iterationExponent = 1;
// const identifier = 0;
// const extendable = true,
const passphrase = Buffer.from('', 'utf-8'); // empty passphrase
const salt = Buffer.alloc(0); // extendable: True => no salt

const half = Math.floor(encryptedSecret.length / 2);
let l = encryptedSecret.subarray(0, half);
let r = encryptedSecret.subarray(half);
for (let round = ROUND_COUNT - 1; round >= 0; round--) {
const f = roundFunction(round, passphrase, iterationExponent, salt, r);
const rr = xor(l, f);
l = r;
r = rr;
}

return Buffer.concat([r, l]);
};

const getEntropy = (options: Options) => {
const data = Buffer.concat([
Buffer.from(options.trezorEntropy, 'hex'),
Buffer.from(options.hostEntropy, 'hex'),
]);
const entropy = sha256(data);
const strength = Math.floor(options.strength / 8);

return Buffer.from(entropy.subarray(0, strength));
};

const computeSeed = (options: Options) => {
const secret = getEntropy(options);
const BackupType = PROTO.Enum_BackupType;
if (
options.type &&
[
BackupType.Slip39_Basic,
BackupType.Slip39_Advanced,
BackupType.Slip39_Single_Extendable,
BackupType.Slip39_Basic_Extendable,
BackupType.Slip39_Advanced_Extendable,
].includes(options.type)
) {
// use slip39
return entropyToSeedSlip39(secret);
}

// use bip39
return mnemonicToSeed(entropyToMnemonic(secret));
};

type Options = {
type?: PROTO.Enum_BackupType;
strength: number;
hostEntropy: string;
trezorEntropy: string;
xpubs: Record<string, string>;
};

export const verifyEntropy = async (options: Options) => {
try {
// compute seed
const seed = await computeSeed(options);

// derive xpubs and compare with FW results
const node = bip32.fromSeed(seed);
Object.keys(options.xpubs).forEach(path => {
const pubKey = node.derivePath(path);
const xpub = pubKey.neutered().toBase58();
if (xpub !== options.xpubs[path]) {
throw new Error('verifyEntropy xpub mismatch');
}
});

return { success: true as const };
} catch (error) {
return { success: false as const, error: error.message };
}
};
65 changes: 63 additions & 2 deletions packages/connect/src/api/resetDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { AbstractMethod } from '../core/AbstractMethod';
import { UI } from '../events';
import { getFirmwareRange } from './common/paramsValidator';
import { PROTO } from '../constants';
import { validatePath } from '../utils/pathUtils';
import { generateEntropy, verifyEntropy } from '../api/firmware/verifyEntropy';

type EntropyRequestData = PROTO.EntropyRequest & { host_entropy: string };

export default class ResetDevice extends AbstractMethod<'resetDevice', PROTO.ResetDevice> {
init() {
Expand All @@ -28,6 +32,8 @@ export default class ResetDevice extends AbstractMethod<'resetDevice', PROTO.Res
skip_backup: payload.skip_backup,
no_backup: payload.no_backup,
backup_type: payload.backup_type,
entropy_check:
typeof payload.entropy_check === 'boolean' ? payload.entropy_check : true,
};
}

Expand All @@ -42,10 +48,65 @@ export default class ResetDevice extends AbstractMethod<'resetDevice', PROTO.Res
};
}

private async getEntropyData(
type: 'ResetDevice' | 'ResetDeviceContinue',
): Promise<EntropyRequestData> {
const cmd = this.device.getCommands();
const entropy = generateEntropy(32).toString('hex');
const params = type === 'ResetDevice' ? this.params : {};
const entropyRequest = await cmd.typedCall(type, 'EntropyRequest', params);
await cmd.typedCall('EntropyAck', 'Success', { entropy });

return {
...entropyRequest.message,
host_entropy: entropy,
};
}

private async entropyCheck(currentData: EntropyRequestData): Promise<EntropyRequestData> {
const cmd = this.device.getCommands();
const paths = ["m/84'/0'/0'", "m/44'/60'/0'"];
const xpubs: Record<string, string> = {}; // <path, xpub>
for (let i = 0; i < paths.length; i++) {
const p = paths[i];
const pubKey = await cmd.getPublicKey({ address_n: validatePath(p) });
xpubs[p] = pubKey.xpub;
}

const entropyData = await this.getEntropyData('ResetDeviceContinue');
const res = await verifyEntropy({
type: this.params.backup_type,
strength: this.params.strength!,
hostEntropy: currentData.host_entropy,
trezorEntropy: entropyData.prev_entropy!,
xpubs,
});
if (res.error) {
throw new Error(res.error);
}

return entropyData;
}

async run() {
const cmd = this.device.getCommands();
const response = await cmd.typedCall('ResetDevice', 'Success', this.params);
// Entropy check workflow:
// https://github.com/trezor/trezor-firmware/blob/andrewkozlik/display_random/docs/common/message-workflows.md#entropy-check-workflow
// steps: 1 - 4
// ResetDevice > EntropyRequest > EntropyAck > Success
let entropyData = await this.getEntropyData('ResetDevice');

// TODO: move this to .init()
if (this.params.entropy_check && !this.device.unavailableCapabilities['entropyCheck']) {
for (let i = 0; i < 3; i++) {
// steps: 5 - 6
// GetPublicKey > ResetDeviceContinue > EntropyRequest > EntropyAck > Success
entropyData = await this.entropyCheck(entropyData);
}
// step 7 ResetDeviceFinish > Success
await cmd.typedCall('ResetDeviceFinish', 'Success', {});
}

return response.message;
return { message: 'Success' };
}
}
4 changes: 4 additions & 0 deletions packages/connect/src/data/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,9 @@ export const config = {
T2B1: '2.7.0',
},
},
{
capabilities: ['entropyCheck'],
min: { T1B1: '0', T2T1: '2.8.4', T2B1: '2.8.4' },
},
],
};
19 changes: 1 addition & 18 deletions packages/connect/src/device/DeviceCommands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// original file https://github.com/trezor/connect/blob/develop/src/js/device/DeviceCommands.js

import { randomBytes } from 'crypto';
// import { randomBytes } from 'crypto';

import { Transport, Session } from '@trezor/transport';
import { MessagesSchema as Messages } from '@trezor/protobuf';
Expand Down Expand Up @@ -43,17 +43,6 @@ const assertType = (res: DefaultPayloadMessage, resType: MessageKey | MessageKey
}
};

const generateEntropy = (len: number) => {
try {
return randomBytes(len);
} catch {
throw ERRORS.TypedError(
'Runtime',
'generateEntropy: Environment does not support crypto random',
);
}
};

const filterForLog = (type: string, msg: any) => {
const blacklist: { [key: string]: Record<string, string> } = {
// PassphraseAck: {
Expand Down Expand Up @@ -431,12 +420,6 @@ export class DeviceCommands {
return this._commonCall('ButtonAck', {});
}

if (res.type === 'EntropyRequest') {
return this._commonCall('EntropyAck', {
entropy: generateEntropy(32).toString('hex'),
});
}

if (res.type === 'PinMatrixRequest') {
return promptPin(this.device, res.message.type).then(
pin =>
Expand Down
9 changes: 9 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5086,6 +5086,13 @@ __metadata:
languageName: node
linkType: hard

"@noble/hashes@npm:^1.6.1":
version: 1.6.1
resolution: "@noble/hashes@npm:1.6.1"
checksum: 10/74d9ad7b1437a22ba3b877584add3367587fbf818113152f293025d20d425aa74c191d18d434797312f2270458bc9ab3241c34d14ec6115fb16438b3248f631f
languageName: node
linkType: hard

"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
Expand Down Expand Up @@ -12011,6 +12018,7 @@ __metadata:
"@ethereumjs/common": "npm:^4.4.0"
"@ethereumjs/tx": "npm:^5.4.0"
"@fivebinaries/coin-selection": "npm:2.2.1"
"@noble/hashes": "npm:^1.6.1"
"@trezor/blockchain-link": "workspace:*"
"@trezor/blockchain-link-types": "workspace:*"
"@trezor/connect-analytics": "workspace:*"
Expand All @@ -12025,6 +12033,7 @@ __metadata:
"@trezor/utxo-lib": "workspace:*"
"@types/karma": "npm:^6.3.8"
babel-loader: "npm:^9.1.3"
bip39: "npm:^3.1.0"
blakejs: "npm:^1.2.1"
bs58: "npm:^6.0.0"
bs58check: "npm:^4.0.0"
Expand Down

0 comments on commit f54ba20

Please sign in to comment.