From: Chris Duncan Date: Sun, 26 Apr 2026 22:44:00 +0000 (-0700) Subject: Require user activation for Tools signing. Clean up click tests. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=e34d7a80cf28a29b662d30df755862511c619e81;p=libnemo.git Require user activation for Tools signing. Clean up click tests. --- diff --git a/src/lib/tools.ts b/src/lib/tools.ts index e010afc..55849dd 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -146,6 +146,9 @@ export class Tools { * @returns {string} 64-byte hexadecimal signature */ static sign (secretKey: string | ArrayBuffer | Uint8Array, ...input: string[]): string { + if (navigator.userActivation?.isActive === false) { + throw new Error('Signing request was blocked due to lack of user activation.') + } const k = this.#normalize(secretKey) try { const signature = nano25519_sign(utf8.toBytes(input.join('')), k) diff --git a/test/test.blocks.mjs b/test/test.blocks.mjs index f78f1f1..1b25277 100644 --- a/test/test.blocks.mjs +++ b/test/test.blocks.mjs @@ -108,12 +108,12 @@ await Promise.all([ const block = new Block(BLAKE2B_ADDRESS_1, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative) .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance) - await assert.resolves(async () => { - await click( - 'Sign with BLAKE2b', - async () => wallet.sign(1, block) - ) - }) + await assert.resolves(click( + 'Sign with BLAKE2b', + async () => wallet.sign(1, block) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.ok(await block.verify(BLAKE2B_PUBLIC_1)) await assert.resolves(wallet.destroy()) @@ -125,12 +125,12 @@ await Promise.all([ const block = new Block(ADDRESS_0, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative) .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance) - await assert.resolves(async () => { - await click( - 'Sign with BIP-44', - async () => wallet.sign(0, block) - ) - }) + await assert.resolves(click( + 'Sign with BIP-44', + async () => wallet.sign(0, block) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.ok(await block.verify(PUBLIC_0)) await assert.resolves(wallet.destroy()) @@ -142,12 +142,12 @@ await Promise.all([ const block = new Block(EXODUS.ADDRESS_0, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative) .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance) - await assert.resolves(async () => { - await click( - 'Sign with Exodus', - async () => wallet.sign(0, block) - ) - }) + await assert.resolves(click( + 'Sign with Exodus', + async () => wallet.sign(0, block) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.ok(await block.verify(EXODUS.PUBLIC_0)) await assert.resolves(wallet.destroy()) @@ -159,8 +159,6 @@ await Promise.all([ const block = new Block(ADDRESS_0, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative) .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance) - console.log('Waiting 6 seconds...') - await new Promise(r => setTimeout(r, 6000)) await assert.rejects(wallet.sign(0, block)) assert.ok(block.signature === undefined) @@ -172,12 +170,12 @@ await Promise.all([ const block = new Block(ADDRESS_0, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative) .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance) - await assert.rejects(async () => { - await click( - 'Fail to sign while locked', - async () => wallet.sign(0, block) - ) - }) + await assert.rejects(click( + 'Fail to sign while locked', + async () => wallet.sign(0, block) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.ok(block.signature === undefined) await wallet.destroy() @@ -187,7 +185,13 @@ await Promise.all([ const block = await new Block(OPEN_BLOCK.account, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative) .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance) .pow(OPEN_BLOCK.work) - await block.sign(OPEN_BLOCK.key) + + await assert.resolves(click( + 'Sign open block with key', + async () => block.sign(OPEN_BLOCK.key) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.equal(block.hash, OPEN_BLOCK.hash) assert.equal(block.signature, OPEN_BLOCK.signature) }) @@ -197,7 +201,12 @@ await Promise.all([ .receive(RECEIVE_BLOCK.link, 0) .pow(RECEIVE_BLOCK.work) - await assert.resolves(async () => await block.sign(RECEIVE_BLOCK.key)) + await assert.resolves(click( + 'Sign receive block with key', + async () => block.sign(RECEIVE_BLOCK.key) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.equal(block.hash, RECEIVE_BLOCK.hash) assert.equal(block.signature, RECEIVE_BLOCK.signature) }) @@ -220,10 +229,17 @@ await Promise.all([ const block = new Block(LEDGER_NANOS.OPEN_BLOCK.account, LEDGER_NANOS.OPEN_BLOCK.balance, LEDGER_NANOS.OPEN_BLOCK.previous, LEDGER_NANOS.OPEN_BLOCK.representative) .receive(LEDGER_NANOS.OPEN_BLOCK.link, LEDGER_NANOS.OPEN_BLOCK.balance) assert.equal(block.hash, LEDGER_NANOS.OPEN_BLOCK.hash) - await wallet.sign(0, block) + + await assert.resolves(click( + 'Sign Ledger-derived block using BIP-44 wallet', + async () => wallet.sign(0, block) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) + assert.equal(block.signature, LEDGER_NANOS.OPEN_BLOCK.signature) assert.ok(await block.verify(account.publicKey)) - await wallet.destroy() + await assert.resolves(wallet.destroy()) }) // skip since nano.org send block sample has receive block work difficulty diff --git a/test/test.ledger.mjs b/test/test.ledger.mjs index 209a112..3189164 100644 --- a/test/test.ledger.mjs +++ b/test/test.ledger.mjs @@ -60,12 +60,10 @@ await Promise.all([ assert.equal(Ledger.status, 'DISCONNECTED') assert.equal(status, 'DISCONNECTED') - await assert.rejects(async () => { - await click( - 'Unlock device, quit Nano app, then click to continue', - async () => wallet.unlock() - ) - }) + await assert.rejects(click( + 'Unlock device, quit Nano app, then click to continue', + async () => wallet.unlock() + )) assert.equal(wallet.isLocked, true) assert.equal(Ledger.status, 'BUSY') assert.equal(status, 'BUSY') @@ -85,12 +83,10 @@ await Promise.all([ }, 6000) }) - await assert.rejects(async () => { - await click( - 'Open Nano app on device, allow device to auto-lock, then click to continue', - async () => wallet.unlock() - ) - }) + await assert.rejects(click( + 'Open Nano app on device, allow device to auto-lock, then click to continue', + async () => wallet.unlock() + )) assert.equal(wallet.isLocked, true) assert.equal(Ledger.status, 'LOCKED') @@ -109,12 +105,10 @@ await Promise.all([ }, 6000) }) - await assert.resolves(async () => { - await click( - 'Unlock device, verify Nano app is open, then click to continue', - async () => wallet.unlock() - ) - }) + await assert.resolves(click( + 'Unlock device, verify Nano app is open, then click to continue', + async () => wallet.unlock() + )) assert.equal(wallet.isLocked, false) assert.equal(Ledger.status, 'CONNECTED') assert.equal(status, 'CONNECTED') @@ -149,42 +143,34 @@ await Promise.all([ }, 90000) }) - await assert.resolves(async () => { - await click( - 'Unlock device again, then click to continue', - async () => { } - ) - }) + await assert.resolves(click( + 'Unlock device again, then click to continue', + async () => { } + )) assert.equal(wallet.isLocked, false) assert.equal(Ledger.status, 'CONNECTED') assert.equal(status, 'CONNECTED') }) await test('switch between interfaces', { skip: false || isNode || navigator?.usb == null }, async () => { - await assert.resolves(async () => { - await click( - 'Verify current interface is HID, switch to unlocked Bluetooth device, then click to continue', - async () => wallet.config({ connection: 'ble' }) - ) - }) + await assert.resolves(click( + 'Verify current interface is HID, switch to unlocked Bluetooth device, then click to continue', + async () => wallet.config({ connection: 'ble' }) + )) assert.equal(wallet.isLocked, true) assert.equal(Ledger.status, 'BUSY') - await assert.resolves(async () => { - await click( - 'Verify current interface is BLE, switch back to unlocked USB device, then click to continue', - async () => wallet.config({ connection: 'usb' }) - ) - }) + await assert.resolves(click( + 'Verify current interface is BLE, switch back to unlocked USB device, then click to continue', + async () => wallet.config({ connection: 'usb' }) + )) assert.equal(wallet.isLocked, false) assert.equal(Ledger.status, 'CONNECTED') - await assert.resolves(async () => { - await click( - 'Verify current interface is USB, then click to continue', - async () => wallet.config({ connection: 'hid' }) - ) - }) + await assert.resolves(click( + 'Verify current interface is USB, then click to continue', + async () => wallet.config({ connection: 'hid' }) + )) assert.equal(wallet.isLocked, false) assert.equal(Ledger.status, 'CONNECTED') }) diff --git a/test/test.tools.mjs b/test/test.tools.mjs index eb04b12..e84ede8 100644 --- a/test/test.tools.mjs +++ b/test/test.tools.mjs @@ -3,14 +3,14 @@ 'use strict' -import { Account, Block, Rpc, Tools, Wallet } from 'libnemo' -import { assert, env, suite, test } from './GLOBALS.mjs' +import { Rpc, Tools, Wallet } from 'libnemo' +import { assert, click, env, suite, test } from './GLOBALS.mjs' import { MAX_RAW, MAX_SUPPLY, NANO_TEST_VECTORS } from './VECTORS.mjs' const rpc = new Rpc(env?.NODE_URL ?? '', env?.API_KEY_NAME) await Promise.all([ - suite('unit conversion tests', async () => { + suite('Tools unit conversion tests', async () => { await test('convert nano to raw', async () => { const result = Tools.convert('1', 'NANO', 'RAW') @@ -98,21 +98,41 @@ await Promise.all([ }) }), - suite('signature tests', async () => { + suite('Tools signature tests', async () => { const m = 'bug-libnemo@' const n = 'codecow.com' - await test('should sign data with a single parameter', async () => { - const result = Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, n) + await test('sign data with a single parameter', async () => { + let result = '' + + await assert.resolves(click( + 'Sign single string', + async () => { + result = Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, n) + } + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) + assert.equal(result.toLowerCase(), '176469a6bea176bc060889225e2d35c484bee7bcfbe32658645de446236121fa2d2dd90e4c895c5fc3e7e9328752fb1e15195cea54964ef5cfe81a6a56dadb02') }) - await test('should sign data with multiple parameters', async () => { - const result = Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, m, n) + await test('sign data with multiple parameters', async () => { + let result = '' + + await assert.resolves(click( + 'Sign multiple strings', + async () => { + result = Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, m, n) + } + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) + assert.equal(result.toLowerCase(), '0ceebbc0b9b3a270a30bad1eef4eddc0931effd8d5c0b7cab007fcc61f30d3ee9760cd93d7eeb5b654ecc0d9d17bc7a6d53bee54aa163a8272f425fb1184e30e') }) - await test('should verify a signature using the public key', async () => { + await test('verify a signature using the public key', async () => { const result = Tools.verify(NANO_TEST_VECTORS.PUBLIC_0, '176469a6bea176bc060889225e2d35c484bee7bcfbe32658645de446236121fa2d2dd90e4c895c5fc3e7e9328752fb1e15195cea54964ef5cfe81a6a56dadb02', n) assert.equal(result, true) diff --git a/test/test.wallet-sign.mjs b/test/test.wallet-sign.mjs index 5763d94..c6f1d6a 100644 --- a/test/test.wallet-sign.mjs +++ b/test/test.wallet-sign.mjs @@ -4,8 +4,8 @@ 'use strict' import { Account, Block, Tools, Wallet } from 'libnemo' -import { assert, env, suite, test } from './GLOBALS.mjs' -import { MAX_RAW, MAX_SUPPLY, NANO_TEST_VECTORS } from './VECTORS.mjs' +import { assert, click, suite, test } from './GLOBALS.mjs' +import { NANO_TEST_VECTORS } from './VECTORS.mjs' await Promise.all([ @@ -16,8 +16,16 @@ await Promise.all([ await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() const data = crypto.randomUUID() - const signature = await wallet.sign(0, data) + let signature = '' + await assert.resolves(click( + 'Sign arbitrary string', + async () => { + signature = await wallet.sign(0, data) + } + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.ok(await Tools.verify(account.publicKey, signature, data)) await assert.resolves(wallet.destroy()) }) @@ -28,7 +36,13 @@ await Promise.all([ const account = await wallet.account() const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou') .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000') - .sign(wallet, 0) + + await assert.resolves(click( + 'Sign to test subsequent verification success', + async () => sendBlock.sign(wallet, 0) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.ok(await sendBlock.verify(account.publicKey)) await assert.resolves(wallet.destroy()) @@ -38,10 +52,16 @@ await Promise.all([ const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() - const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou') .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000') - .sign(wallet, 0) + + await assert.resolves(click( + 'Sign to test subsequent verification failure', + async () => sendBlock.sign(wallet, 0) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) + assert.ok(await sendBlock.verify(account.publicKey)) const wrongAccount = new Account('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p')