From 1ed448a0d876b220d04615d7ea1340f836c2ccd9 Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 10 Feb 2025 11:23:48 +0200 Subject: [PATCH 01/12] impl entropy generator and add extra entropy to the addSecret method --- src/controllers/keystore/keystore.test.ts | 6 +-- src/controllers/keystore/keystore.ts | 38 +++++++--------- src/libs/entropyGenerator/entropyGenerator.ts | 45 +++++++++++++++++++ 3 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 src/libs/entropyGenerator/entropyGenerator.ts diff --git a/src/controllers/keystore/keystore.test.ts b/src/controllers/keystore/keystore.test.ts index b166f001a..0fef51d1e 100644 --- a/src/controllers/keystore/keystore.test.ts +++ b/src/controllers/keystore/keystore.test.ts @@ -107,7 +107,7 @@ describe('KeystoreController', () => { }) test('should add a secret', (done) => { - keystore.addSecret('password', pass, '', false) + keystore.addSecret('password', pass, null, false) const unsubscribe = keystore.onUpdate(async () => { if (keystore.statuses.addSecret === 'SUCCESS') { @@ -556,11 +556,11 @@ describe('import/export with pub key test', () => { keystore = new KeystoreController(produceMemoryStore(), keystoreSigners, windowManager) keystore2 = new KeystoreController(produceMemoryStore(), keystoreSigners, windowManager) - await keystore2.addSecret('123', '123', '', false) + await keystore2.addSecret('123', '123', null, false) await keystore2.unlockWithSecret('123', '123') uid2 = await keystore2.getKeyStoreUid() - await keystore.addSecret('a', 'b', '', false) + await keystore.addSecret('a', 'b', null, false) await keystore.unlockWithSecret('a', 'b') }) diff --git a/src/controllers/keystore/keystore.ts b/src/controllers/keystore/keystore.ts index 529f0b035..f5cf5d627 100644 --- a/src/controllers/keystore/keystore.ts +++ b/src/controllers/keystore/keystore.ts @@ -9,16 +9,7 @@ import { encryptWithPublicKey, publicKeyByPrivateKey } from 'eth-crypto' -import { - concat, - getBytes, - hexlify, - keccak256, - Mnemonic, - randomBytes, - toUtf8Bytes, - Wallet -} from 'ethers' +import { concat, getBytes, hexlify, keccak256, Mnemonic, toUtf8Bytes, Wallet } from 'ethers' import scrypt from 'scrypt-js' import EmittableError from '../../classes/EmittableError' @@ -38,6 +29,7 @@ import { } from '../../interfaces/keystore' import { Storage } from '../../interfaces/storage' import { WindowManager } from '../../interfaces/window' +import { EntropyGenerator } from '../../libs/entropyGenerator/entropyGenerator' import { getDefaultKeyLabel, getShouldMigrateKeyMetaNullToKeyMetaCreatedAt, @@ -116,6 +108,8 @@ export class KeystoreController extends EventEmitter { #keystoreKeys: StoredKey[] = [] + #entropyGenerator = new EntropyGenerator() + keyStoreUid: string | null isReadyToStoreKeys: boolean = false @@ -287,7 +281,7 @@ export class KeystoreController extends EventEmitter { async #addSecret( secretId: string, secret: string, - extraEntropy: string = '', + extraEntropy: Uint8Array | null = null, leaveUnlocked: boolean = false ) { await this.#initialLoadPromise @@ -301,16 +295,13 @@ export class KeystoreController extends EventEmitter { }) let mainKey: MainKey | null = this.#mainKey + // We are not unlocked if (!mainKey) { if (!this.#keystoreSecrets.length) { - const key = getBytes(keccak256(concat([randomBytes(32), toUtf8Bytes(extraEntropy)]))).slice( - 0, - 16 - ) mainKey = { - key, - iv: randomBytes(16) + key: this.#entropyGenerator.generateRandomBytes(16, extraEntropy), + iv: this.#entropyGenerator.generateRandomBytes(16, extraEntropy) } } else throw new EmittableError({ @@ -324,7 +315,7 @@ export class KeystoreController extends EventEmitter { } } - const salt = randomBytes(32) + const salt = this.#entropyGenerator.generateRandomBytes(32, extraEntropy) const key = await scrypt.scrypt( getBytesForSecret(secret), salt, @@ -334,7 +325,7 @@ export class KeystoreController extends EventEmitter { scryptDefaults.dkLen, () => {} ) - const iv = randomBytes(16) + const iv = this.#entropyGenerator.generateRandomBytes(16, extraEntropy) const derivedKey = key.slice(0, 16) const macPrefix = key.slice(16, 32) const counter = new aes.Counter(iv) @@ -365,7 +356,12 @@ export class KeystoreController extends EventEmitter { this.isReadyToStoreKeys = true } - async addSecret(secretId: string, secret: string, extraEntropy: string, leaveUnlocked: boolean) { + async addSecret( + secretId: string, + secret: string, + extraEntropy: Uint8Array | null, + leaveUnlocked: boolean + ) { await this.withStatus('addSecret', () => this.#addSecret(secretId, secret, extraEntropy, leaveUnlocked) ) @@ -901,7 +897,7 @@ export class KeystoreController extends EventEmitter { }) await this.#removeSecret('password') - await this.#addSecret('password', newSecret, '', true) + await this.#addSecret('password', newSecret, null, true) } async changeKeystorePassword(newSecret: string, oldSecret?: string) { diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts new file mode 100644 index 000000000..787200912 --- /dev/null +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-bitwise */ +import { getBytes, keccak256, randomBytes } from 'ethers' + +export class EntropyGenerator { + #entropyPool: Uint8Array = new Uint8Array(0) + + generateRandomBytes(length: number, extraEntropy: Uint8Array | null): Uint8Array { + this.#addSystemNoiseEntropy() + this.#addTimeEntropy() + + if (extraEntropy) this.addEntropy(extraEntropy) + + if (this.#entropyPool.length === 0) throw new Error('Entropy pool is empty') + + const hash = getBytes(keccak256(this.#entropyPool)) + const randomBytesGenerated = randomBytes(length) + for (let i = 0; i < length; i++) { + randomBytesGenerated[i] ^= hash[i % hash.length] + } + + return randomBytesGenerated + } + + #addTimeEntropy(): void { + const now = performance.now() + + if (!now) return + + const timeEntropy = new Uint8Array(new Float64Array([now]).buffer) + console.log('addTimeEntropy', timeEntropy) + this.addEntropy(timeEntropy) + } + + #addSystemNoiseEntropy(): void { + const systemNoise = randomBytes(16) + this.addEntropy(systemNoise) + } + + addEntropy(newEntropy: Uint8Array): void { + const combined = new Uint8Array(this.#entropyPool.length + newEntropy.length) + combined.set(this.#entropyPool) + combined.set(newEntropy, this.#entropyPool.length) + this.#entropyPool = combined + } +} From 8b4477be9b9dd150564d498c72b4867d2e0bc596 Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 10 Feb 2025 13:39:40 +0200 Subject: [PATCH 02/12] extra entropy improvements --- src/controllers/keystore/keystore.test.ts | 6 ++--- src/controllers/keystore/keystore.ts | 22 +++++++------------ src/libs/entropyGenerator/entropyGenerator.ts | 14 +++++++++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/controllers/keystore/keystore.test.ts b/src/controllers/keystore/keystore.test.ts index 0fef51d1e..b166f001a 100644 --- a/src/controllers/keystore/keystore.test.ts +++ b/src/controllers/keystore/keystore.test.ts @@ -107,7 +107,7 @@ describe('KeystoreController', () => { }) test('should add a secret', (done) => { - keystore.addSecret('password', pass, null, false) + keystore.addSecret('password', pass, '', false) const unsubscribe = keystore.onUpdate(async () => { if (keystore.statuses.addSecret === 'SUCCESS') { @@ -556,11 +556,11 @@ describe('import/export with pub key test', () => { keystore = new KeystoreController(produceMemoryStore(), keystoreSigners, windowManager) keystore2 = new KeystoreController(produceMemoryStore(), keystoreSigners, windowManager) - await keystore2.addSecret('123', '123', null, false) + await keystore2.addSecret('123', '123', '', false) await keystore2.unlockWithSecret('123', '123') uid2 = await keystore2.getKeyStoreUid() - await keystore.addSecret('a', 'b', null, false) + await keystore.addSecret('a', 'b', '', false) await keystore.unlockWithSecret('a', 'b') }) diff --git a/src/controllers/keystore/keystore.ts b/src/controllers/keystore/keystore.ts index f5cf5d627..165fd7b34 100644 --- a/src/controllers/keystore/keystore.ts +++ b/src/controllers/keystore/keystore.ts @@ -108,8 +108,6 @@ export class KeystoreController extends EventEmitter { #keystoreKeys: StoredKey[] = [] - #entropyGenerator = new EntropyGenerator() - keyStoreUid: string | null isReadyToStoreKeys: boolean = false @@ -281,7 +279,7 @@ export class KeystoreController extends EventEmitter { async #addSecret( secretId: string, secret: string, - extraEntropy: Uint8Array | null = null, + extraEntropy: string = '', leaveUnlocked: boolean = false ) { await this.#initialLoadPromise @@ -295,13 +293,14 @@ export class KeystoreController extends EventEmitter { }) let mainKey: MainKey | null = this.#mainKey + const entropyGenerator = new EntropyGenerator() // We are not unlocked if (!mainKey) { if (!this.#keystoreSecrets.length) { mainKey = { - key: this.#entropyGenerator.generateRandomBytes(16, extraEntropy), - iv: this.#entropyGenerator.generateRandomBytes(16, extraEntropy) + key: entropyGenerator.generateRandomBytes(16, extraEntropy), + iv: entropyGenerator.generateRandomBytes(16, extraEntropy) } } else throw new EmittableError({ @@ -315,7 +314,7 @@ export class KeystoreController extends EventEmitter { } } - const salt = this.#entropyGenerator.generateRandomBytes(32, extraEntropy) + const salt = entropyGenerator.generateRandomBytes(32, extraEntropy) const key = await scrypt.scrypt( getBytesForSecret(secret), salt, @@ -325,7 +324,7 @@ export class KeystoreController extends EventEmitter { scryptDefaults.dkLen, () => {} ) - const iv = this.#entropyGenerator.generateRandomBytes(16, extraEntropy) + const iv = entropyGenerator.generateRandomBytes(16, extraEntropy) const derivedKey = key.slice(0, 16) const macPrefix = key.slice(16, 32) const counter = new aes.Counter(iv) @@ -356,12 +355,7 @@ export class KeystoreController extends EventEmitter { this.isReadyToStoreKeys = true } - async addSecret( - secretId: string, - secret: string, - extraEntropy: Uint8Array | null, - leaveUnlocked: boolean - ) { + async addSecret(secretId: string, secret: string, extraEntropy: string, leaveUnlocked: boolean) { await this.withStatus('addSecret', () => this.#addSecret(secretId, secret, extraEntropy, leaveUnlocked) ) @@ -897,7 +891,7 @@ export class KeystoreController extends EventEmitter { }) await this.#removeSecret('password') - await this.#addSecret('password', newSecret, null, true) + await this.#addSecret('password', newSecret, '', true) } async changeKeystorePassword(newSecret: string, oldSecret?: string) { diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index 787200912..3a5d565fd 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -4,11 +4,16 @@ import { getBytes, keccak256, randomBytes } from 'ethers' export class EntropyGenerator { #entropyPool: Uint8Array = new Uint8Array(0) - generateRandomBytes(length: number, extraEntropy: Uint8Array | null): Uint8Array { + generateRandomBytes(length: number, extraEntropy: string): Uint8Array { + this.#resetEntropyPool() this.#addSystemNoiseEntropy() this.#addTimeEntropy() - if (extraEntropy) this.addEntropy(extraEntropy) + if (extraEntropy) { + const encoder = new TextEncoder() + const uint8Array = encoder.encode(extraEntropy) + this.addEntropy(uint8Array) + } if (this.#entropyPool.length === 0) throw new Error('Entropy pool is empty') @@ -27,7 +32,6 @@ export class EntropyGenerator { if (!now) return const timeEntropy = new Uint8Array(new Float64Array([now]).buffer) - console.log('addTimeEntropy', timeEntropy) this.addEntropy(timeEntropy) } @@ -42,4 +46,8 @@ export class EntropyGenerator { combined.set(newEntropy, this.#entropyPool.length) this.#entropyPool = combined } + + #resetEntropyPool() { + this.#entropyPool = new Uint8Array(0) + } } From f40742bbc2ddd0128efc35ba6a8a3bb6a8623457 Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 10 Feb 2025 14:57:43 +0200 Subject: [PATCH 03/12] add comment --- src/libs/entropyGenerator/entropyGenerator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index 3a5d565fd..159b9ac21 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -19,6 +19,7 @@ export class EntropyGenerator { const hash = getBytes(keccak256(this.#entropyPool)) const randomBytesGenerated = randomBytes(length) + // ensures non-deterministic final output for (let i = 0; i < length; i++) { randomBytesGenerated[i] ^= hash[i % hash.length] } From a65a59a20bb023342a76ce35711e5819ee2fc0e1 Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 10 Feb 2025 15:20:07 +0200 Subject: [PATCH 04/12] rename functions --- src/libs/entropyGenerator/entropyGenerator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index 159b9ac21..e103b94d7 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -6,8 +6,8 @@ export class EntropyGenerator { generateRandomBytes(length: number, extraEntropy: string): Uint8Array { this.#resetEntropyPool() - this.#addSystemNoiseEntropy() - this.#addTimeEntropy() + this.#collectSystemNoiseEntropy() + this.#collectTimeEntropy() if (extraEntropy) { const encoder = new TextEncoder() @@ -27,7 +27,7 @@ export class EntropyGenerator { return randomBytesGenerated } - #addTimeEntropy(): void { + #collectTimeEntropy(): void { const now = performance.now() if (!now) return @@ -36,7 +36,7 @@ export class EntropyGenerator { this.addEntropy(timeEntropy) } - #addSystemNoiseEntropy(): void { + #collectSystemNoiseEntropy(): void { const systemNoise = randomBytes(16) this.addEntropy(systemNoise) } From 8ef052ca37689b3973ab82afed584c1f96522b3e Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 17 Feb 2025 14:31:45 +0200 Subject: [PATCH 05/12] add: entropyGenerator tests --- .../entropyGenerator/entropyGenerator.test.ts | 57 +++++++++++++++++++ src/libs/entropyGenerator/entropyGenerator.ts | 7 +-- 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/libs/entropyGenerator/entropyGenerator.test.ts diff --git a/src/libs/entropyGenerator/entropyGenerator.test.ts b/src/libs/entropyGenerator/entropyGenerator.test.ts new file mode 100644 index 000000000..6a42163a1 --- /dev/null +++ b/src/libs/entropyGenerator/entropyGenerator.test.ts @@ -0,0 +1,57 @@ +import { EntropyGenerator } from './entropyGenerator' + +describe('EntropyGenerator', () => { + let generator: EntropyGenerator + + beforeEach(() => { + generator = new EntropyGenerator() + }) + + test('should generate random bytes with extra entropy', () => { + const length = 32 + const extraEntropy = 'extra randomness' + const result = generator.generateRandomBytes(length, extraEntropy) + + expect(result).toBeInstanceOf(Uint8Array) + expect(result.length).toBe(length) + }) + test('should throw an error when entropy pool is empty', () => { + jest.spyOn(generator, 'addEntropy').mockImplementation(() => {}) + expect(() => generator.generateRandomBytes(16, '')).toThrow('Entropy pool is empty') + }) + test('should collect time entropy', () => { + jest.spyOn(generator, 'addEntropy') + generator.generateRandomBytes(16, 'test') + expect(generator.addEntropy).toHaveBeenCalled() + }) + test('should collect system noise entropy', () => { + jest.spyOn(generator, 'addEntropy') + generator.generateRandomBytes(16, 'test') + expect(generator.addEntropy).toHaveBeenCalled() + }) + test('should produce different outputs on consecutive calls', () => { + const length = 32 + const extraEntropy = 'extra randomness' + const result1 = generator.generateRandomBytes(length, extraEntropy) + const result2 = generator.generateRandomBytes(length, extraEntropy) + expect(result1).not.toEqual(result2) + }) + test('should ensure randomness by checking uniform distribution', () => { + const length = 32 + const occurrences = new Map() + for (let i = 0; i < 1000; i++) { + const result = generator.generateRandomBytes(length, 'entropy-test') + occurrences.set(result.toString(), (occurrences.get(result.toString()) || 0) + 1) + } + expect(occurrences.size).toBeGreaterThan(999) // Expect at least 999 unique values out of 1000 + }) + test('should not produce predictable patterns', () => { + const length = 32 + const results: Uint8Array[] = [] + for (let i = 0; i < 100; i++) { + results.push(generator.generateRandomBytes(length, '')) + } + const diffs = results.map((r, i) => (i > 0 ? r.toString() !== results[i - 1].toString() : true)) + expect(diffs.includes(false)).toBe(false) + }) +}) diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index e103b94d7..2f27424cd 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -6,7 +6,7 @@ export class EntropyGenerator { generateRandomBytes(length: number, extraEntropy: string): Uint8Array { this.#resetEntropyPool() - this.#collectSystemNoiseEntropy() + this.#collectSystemNoiseEntropy(length) this.#collectTimeEntropy() if (extraEntropy) { @@ -36,9 +36,8 @@ export class EntropyGenerator { this.addEntropy(timeEntropy) } - #collectSystemNoiseEntropy(): void { - const systemNoise = randomBytes(16) - this.addEntropy(systemNoise) + #collectSystemNoiseEntropy(length: number): void { + this.addEntropy(randomBytes(length)) } addEntropy(newEntropy: Uint8Array): void { From a8655e29478300597857fb5898614271923097b7 Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 17 Feb 2025 14:54:08 +0200 Subject: [PATCH 06/12] add: extra entropy to the change keystore function --- src/controllers/keystore/keystore.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/keystore/keystore.ts b/src/controllers/keystore/keystore.ts index 165fd7b34..f1dcc9e16 100644 --- a/src/controllers/keystore/keystore.ts +++ b/src/controllers/keystore/keystore.ts @@ -861,7 +861,7 @@ export class KeystoreController extends EventEmitter { return { seed: decryptedSeed, hdPathTemplate } } - async #changeKeystorePassword(newSecret: string, oldSecret?: string) { + async #changeKeystorePassword(newSecret: string, oldSecret?: string, extraEntropy?: string) { await this.#initialLoadPromise // In the case the user wants to change their device password, @@ -891,12 +891,12 @@ export class KeystoreController extends EventEmitter { }) await this.#removeSecret('password') - await this.#addSecret('password', newSecret, '', true) + await this.#addSecret('password', newSecret, extraEntropy, true) } - async changeKeystorePassword(newSecret: string, oldSecret?: string) { + async changeKeystorePassword(newSecret: string, oldSecret?: string, extraEntropy?: string) { await this.withStatus('changeKeystorePassword', () => - this.#changeKeystorePassword(newSecret, oldSecret) + this.#changeKeystorePassword(newSecret, oldSecret, extraEntropy) ) } From a3c41a5597157b30467100e26e42a67aea141e85 Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 17 Feb 2025 16:24:55 +0200 Subject: [PATCH 07/12] add: generateRandomMnemonic --- src/libs/entropyGenerator/entropyGenerator.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index 2f27424cd..866b58364 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -1,5 +1,5 @@ /* eslint-disable no-bitwise */ -import { getBytes, keccak256, randomBytes } from 'ethers' +import { getBytes, keccak256, LangEn, Mnemonic, randomBytes } from 'ethers' export class EntropyGenerator { #entropyPool: Uint8Array = new Uint8Array(0) @@ -27,6 +27,14 @@ export class EntropyGenerator { return randomBytesGenerated } + generateRandomMnemonic(wordCount: 12 | 24, extraEntropy: string): Mnemonic { + const wordCountToBytesLength = { 12: 16, 24: 32 } + const bytesLength = wordCountToBytesLength[wordCount] || 16 + const entropy = this.generateRandomBytes(bytesLength, extraEntropy) + const mnemonic = Mnemonic.fromEntropy(entropy, '', LangEn.wordlist()) + return mnemonic + } + #collectTimeEntropy(): void { const now = performance.now() From 1cd2eb7f8d7256403a0691a8b7b9b6b91edc533f Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 17 Feb 2025 17:29:26 +0200 Subject: [PATCH 08/12] add more tests --- .../entropyGenerator/entropyGenerator.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/libs/entropyGenerator/entropyGenerator.test.ts b/src/libs/entropyGenerator/entropyGenerator.test.ts index 6a42163a1..ea9f828dc 100644 --- a/src/libs/entropyGenerator/entropyGenerator.test.ts +++ b/src/libs/entropyGenerator/entropyGenerator.test.ts @@ -54,4 +54,29 @@ describe('EntropyGenerator', () => { const diffs = results.map((r, i) => (i > 0 ? r.toString() !== results[i - 1].toString() : true)) expect(diffs.includes(false)).toBe(false) }) + test('should generate a valid mnemonic of 12 words', () => { + const mnemonic = generator.generateRandomMnemonic(12, 'extra entropy') + expect(mnemonic).toHaveProperty('phrase') + expect(mnemonic.phrase.split(' ').length).toBe(12) + }) + test('should generate a valid mnemonic of 24 words', () => { + const mnemonic = generator.generateRandomMnemonic(24, 'extra entropy') + expect(mnemonic).toHaveProperty('phrase') + expect(mnemonic.phrase.split(' ').length).toBe(24) + }) + test('should generate different mnemonics on consecutive calls', () => { + const mnemonic1 = generator.generateRandomMnemonic(12, 'extra entropy') + const mnemonic2 = generator.generateRandomMnemonic(12, 'extra entropy') + expect(mnemonic1.phrase).not.toEqual(mnemonic2.phrase) + }) + test('should use correct entropy length for 12-word mnemonic', () => { + jest.spyOn(generator, 'generateRandomBytes') + generator.generateRandomMnemonic(12, 'extra entropy') + expect(generator.generateRandomBytes).toHaveBeenCalledWith(16, 'extra entropy') + }) + test('should use correct entropy length for 24-word mnemonic', () => { + jest.spyOn(generator, 'generateRandomBytes') + generator.generateRandomMnemonic(24, 'extra entropy') + expect(generator.generateRandomBytes).toHaveBeenCalledWith(32, 'extra entropy') + }) }) From 6c2924e69143ebf92bc16cd3f9c5faeab631ef85 Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 17 Feb 2025 17:44:10 +0200 Subject: [PATCH 09/12] feedpack fixes and optimizations --- src/libs/entropyGenerator/entropyGenerator.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index 866b58364..9cb8cbd77 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -6,7 +6,7 @@ export class EntropyGenerator { generateRandomBytes(length: number, extraEntropy: string): Uint8Array { this.#resetEntropyPool() - this.#collectSystemNoiseEntropy(length) + this.#collectCryptographicEntropy(length) this.#collectTimeEntropy() if (extraEntropy) { @@ -19,7 +19,7 @@ export class EntropyGenerator { const hash = getBytes(keccak256(this.#entropyPool)) const randomBytesGenerated = randomBytes(length) - // ensures non-deterministic final output + // Introduces additional entropy mixing via XOR for (let i = 0; i < length; i++) { randomBytesGenerated[i] ^= hash[i % hash.length] } @@ -44,15 +44,12 @@ export class EntropyGenerator { this.addEntropy(timeEntropy) } - #collectSystemNoiseEntropy(length: number): void { + #collectCryptographicEntropy(length: number): void { this.addEntropy(randomBytes(length)) } addEntropy(newEntropy: Uint8Array): void { - const combined = new Uint8Array(this.#entropyPool.length + newEntropy.length) - combined.set(this.#entropyPool) - combined.set(newEntropy, this.#entropyPool.length) - this.#entropyPool = combined + this.#entropyPool = new Uint8Array(Buffer.concat([this.#entropyPool, newEntropy])) } #resetEntropyPool() { From fdb7bb224a79ee5b4e153767263d83ebb35c6b3b Mon Sep 17 00:00:00 2001 From: sonytooo Date: Mon, 17 Feb 2025 17:46:16 +0200 Subject: [PATCH 10/12] add: comment --- src/libs/entropyGenerator/entropyGenerator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index 9cb8cbd77..8f916e769 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -29,7 +29,7 @@ export class EntropyGenerator { generateRandomMnemonic(wordCount: 12 | 24, extraEntropy: string): Mnemonic { const wordCountToBytesLength = { 12: 16, 24: 32 } - const bytesLength = wordCountToBytesLength[wordCount] || 16 + const bytesLength = wordCountToBytesLength[wordCount] || 16 // defaults to 12-word phrase const entropy = this.generateRandomBytes(bytesLength, extraEntropy) const mnemonic = Mnemonic.fromEntropy(entropy, '', LangEn.wordlist()) return mnemonic From 8dc02b519ab6a4869039c7a43c4c22c0e897b87b Mon Sep 17 00:00:00 2001 From: sonytooo Date: Wed, 19 Feb 2025 11:56:32 +0200 Subject: [PATCH 11/12] feedback fixes --- src/libs/entropyGenerator/entropyGenerator.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index 8f916e769..d69367187 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -1,6 +1,10 @@ /* eslint-disable no-bitwise */ import { getBytes, keccak256, LangEn, Mnemonic, randomBytes } from 'ethers' +// Custom entropy generator that enhances ethers' randomBytes by incorporating: +// - Time-based entropy for additional randomness. +// - Optional extra entropy (like mouse position, timestamp...) provided by the user for added security. +// This helps improve the security of mainKey generation and random seed phrase creation. export class EntropyGenerator { #entropyPool: Uint8Array = new Uint8Array(0) @@ -36,6 +40,7 @@ export class EntropyGenerator { } #collectTimeEntropy(): void { + // TODO: add a polyfill for the mobile app const now = performance.now() if (!now) return From f3a980b01c91b42cb77d9270199b96410c5b846a Mon Sep 17 00:00:00 2001 From: sonytooo Date: Wed, 19 Feb 2025 12:14:14 +0200 Subject: [PATCH 12/12] improve commnet --- src/libs/entropyGenerator/entropyGenerator.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/entropyGenerator/entropyGenerator.ts b/src/libs/entropyGenerator/entropyGenerator.ts index d69367187..38f3adc0a 100644 --- a/src/libs/entropyGenerator/entropyGenerator.ts +++ b/src/libs/entropyGenerator/entropyGenerator.ts @@ -40,7 +40,12 @@ export class EntropyGenerator { } #collectTimeEntropy(): void { - // TODO: add a polyfill for the mobile app + // TODO: steps to add support for the mobile app: + // 1. install the polyfill: `yarn add react-native-performance` + // 2. add it globally in a top-level file: + // if (typeof performance === "undefined") { + // global.performance = { now } + // } const now = performance.now() if (!now) return