]> git.codecow.com Git - libnemo.git/commitdiff
Require user activation for Tools signing. Clean up click tests.
authorChris Duncan <chris@zoso.dev>
Sun, 26 Apr 2026 22:44:00 +0000 (15:44 -0700)
committerChris Duncan <chris@zoso.dev>
Sun, 26 Apr 2026 22:44:00 +0000 (15:44 -0700)
src/lib/tools.ts
test/test.blocks.mjs
test/test.ledger.mjs
test/test.tools.mjs
test/test.wallet-sign.mjs

index e010afcdda366c29aa59ba08438c8489cabeeb9d..55849dd7711e9251861bf2048c1fc9448861c1cb 100644 (file)
@@ -146,6 +146,9 @@ export class Tools {
         * @returns {string} 64-byte hexadecimal signature
         */
        static sign (secretKey: string | ArrayBuffer | Uint8Array<ArrayBuffer>, ...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)
index f78f1f1afece59efe714876cc7783e38d8bed85f..1b2527700170dcb00fd74c0e039c1d56a964fcbb 100644 (file)
@@ -108,12 +108,12 @@ await Promise.all([
                        const block = new Block(BLAKE2B_ADDRESS_1, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative)\r
                                .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance)\r
 \r
-                       await assert.resolves(async () => {\r
-                               await click(\r
-                                       'Sign with BLAKE2b',\r
-                                       async () => wallet.sign(1, block)\r
-                               )\r
-                       })\r
+                       await assert.resolves(click(\r
+                               'Sign with BLAKE2b',\r
+                               async () => wallet.sign(1, block)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
                        assert.ok(await block.verify(BLAKE2B_PUBLIC_1))\r
 \r
                        await assert.resolves(wallet.destroy())\r
@@ -125,12 +125,12 @@ await Promise.all([
                        const block = new Block(ADDRESS_0, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative)\r
                                .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance)\r
 \r
-                       await assert.resolves(async () => {\r
-                               await click(\r
-                                       'Sign with BIP-44',\r
-                                       async () => wallet.sign(0, block)\r
-                               )\r
-                       })\r
+                       await assert.resolves(click(\r
+                               'Sign with BIP-44',\r
+                               async () => wallet.sign(0, block)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
                        assert.ok(await block.verify(PUBLIC_0))\r
 \r
                        await assert.resolves(wallet.destroy())\r
@@ -142,12 +142,12 @@ await Promise.all([
                        const block = new Block(EXODUS.ADDRESS_0, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative)\r
                                .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance)\r
 \r
-                       await assert.resolves(async () => {\r
-                               await click(\r
-                                       'Sign with Exodus',\r
-                                       async () => wallet.sign(0, block)\r
-                               )\r
-                       })\r
+                       await assert.resolves(click(\r
+                               'Sign with Exodus',\r
+                               async () => wallet.sign(0, block)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
                        assert.ok(await block.verify(EXODUS.PUBLIC_0))\r
 \r
                        await assert.resolves(wallet.destroy())\r
@@ -159,8 +159,6 @@ await Promise.all([
                        const block = new Block(ADDRESS_0, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative)\r
                                .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance)\r
 \r
-                       console.log('Waiting 6 seconds...')\r
-                       await new Promise(r => setTimeout(r, 6000))\r
                        await assert.rejects(wallet.sign(0, block))\r
                        assert.ok(block.signature === undefined)\r
 \r
@@ -172,12 +170,12 @@ await Promise.all([
                        const block = new Block(ADDRESS_0, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative)\r
                                .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance)\r
 \r
-                       await assert.rejects(async () => {\r
-                               await click(\r
-                                       'Fail to sign while locked',\r
-                                       async () => wallet.sign(0, block)\r
-                               )\r
-                       })\r
+                       await assert.rejects(click(\r
+                               'Fail to sign while locked',\r
+                               async () => wallet.sign(0, block)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
                        assert.ok(block.signature === undefined)\r
 \r
                        await wallet.destroy()\r
@@ -187,7 +185,13 @@ await Promise.all([
                        const block = await new Block(OPEN_BLOCK.account, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative)\r
                                .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance)\r
                                .pow(OPEN_BLOCK.work)\r
-                       await block.sign(OPEN_BLOCK.key)\r
+\r
+                       await assert.resolves(click(\r
+                               'Sign open block with key',\r
+                               async () => block.sign(OPEN_BLOCK.key)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
                        assert.equal(block.hash, OPEN_BLOCK.hash)\r
                        assert.equal(block.signature, OPEN_BLOCK.signature)\r
                })\r
@@ -197,7 +201,12 @@ await Promise.all([
                                .receive(RECEIVE_BLOCK.link, 0)\r
                                .pow(RECEIVE_BLOCK.work)\r
 \r
-                       await assert.resolves(async () => await block.sign(RECEIVE_BLOCK.key))\r
+                       await assert.resolves(click(\r
+                               'Sign receive block with key',\r
+                               async () => block.sign(RECEIVE_BLOCK.key)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
                        assert.equal(block.hash, RECEIVE_BLOCK.hash)\r
                        assert.equal(block.signature, RECEIVE_BLOCK.signature)\r
                })\r
@@ -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)\r
                                .receive(LEDGER_NANOS.OPEN_BLOCK.link, LEDGER_NANOS.OPEN_BLOCK.balance)\r
                        assert.equal(block.hash, LEDGER_NANOS.OPEN_BLOCK.hash)\r
-                       await wallet.sign(0, block)\r
+\r
+                       await assert.resolves(click(\r
+                               'Sign Ledger-derived block using BIP-44 wallet',\r
+                               async () => wallet.sign(0, block)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
+\r
                        assert.equal(block.signature, LEDGER_NANOS.OPEN_BLOCK.signature)\r
                        assert.ok(await block.verify(account.publicKey))\r
-                       await wallet.destroy()\r
+                       await assert.resolves(wallet.destroy())\r
                })\r
 \r
                // skip since nano.org send block sample has receive block work difficulty\r
index 209a112144742a7f5cf2e765dfecbd1e06ee5dbe..31891642b134314ba88db9efb4e78e69c3da13d1 100644 (file)
@@ -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')
                })
index eb04b12012c95949caa62009288597c921cc31f3..e84ede8d0bc5e18d56d25c6ddb254857aab81e4c 100644 (file)
@@ -3,14 +3,14 @@
 \r
 'use strict'\r
 \r
-import { Account, Block, Rpc, Tools, Wallet } from 'libnemo'\r
-import { assert, env, suite, test } from './GLOBALS.mjs'\r
+import { Rpc, Tools, Wallet } from 'libnemo'\r
+import { assert, click, env, suite, test } from './GLOBALS.mjs'\r
 import { MAX_RAW, MAX_SUPPLY, NANO_TEST_VECTORS } from './VECTORS.mjs'\r
 \r
 const rpc = new Rpc(env?.NODE_URL ?? '', env?.API_KEY_NAME)\r
 \r
 await Promise.all([\r
-       suite('unit conversion tests', async () => {\r
+       suite('Tools unit conversion tests', async () => {\r
 \r
                await test('convert nano to raw', async () => {\r
                        const result = Tools.convert('1', 'NANO', 'RAW')\r
@@ -98,21 +98,41 @@ await Promise.all([
                })\r
        }),\r
 \r
-       suite('signature tests', async () => {\r
+       suite('Tools signature tests', async () => {\r
                const m = 'bug-libnemo@'\r
                const n = 'codecow.com'\r
 \r
-               await test('should sign data with a single parameter', async () => {\r
-                       const result = Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, n)\r
+               await test('sign data with a single parameter', async () => {\r
+                       let result = ''\r
+\r
+                       await assert.resolves(click(\r
+                               'Sign single string',\r
+                               async () => {\r
+                                       result = Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, n)\r
+                               }\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
+\r
                        assert.equal(result.toLowerCase(), '176469a6bea176bc060889225e2d35c484bee7bcfbe32658645de446236121fa2d2dd90e4c895c5fc3e7e9328752fb1e15195cea54964ef5cfe81a6a56dadb02')\r
                })\r
 \r
-               await test('should sign data with multiple parameters', async () => {\r
-                       const result = Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, m, n)\r
+               await test('sign data with multiple parameters', async () => {\r
+                       let result = ''\r
+\r
+                       await assert.resolves(click(\r
+                               'Sign multiple strings',\r
+                               async () => {\r
+                                       result = Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, m, n)\r
+                               }\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
+\r
                        assert.equal(result.toLowerCase(), '0ceebbc0b9b3a270a30bad1eef4eddc0931effd8d5c0b7cab007fcc61f30d3ee9760cd93d7eeb5b654ecc0d9d17bc7a6d53bee54aa163a8272f425fb1184e30e')\r
                })\r
 \r
-               await test('should verify a signature using the public key', async () => {\r
+               await test('verify a signature using the public key', async () => {\r
                        const result = Tools.verify(NANO_TEST_VECTORS.PUBLIC_0, '176469a6bea176bc060889225e2d35c484bee7bcfbe32658645de446236121fa2d2dd90e4c895c5fc3e7e9328752fb1e15195cea54964ef5cfe81a6a56dadb02', n)\r
                        assert.equal(result, true)\r
 \r
index 5763d94a5513b09d47a3b200646955d9279aae19..c6f1d6a7bf411c7519d6c20fb44524bbc1b3bfb2 100644 (file)
@@ -4,8 +4,8 @@
 'use strict'\r
 \r
 import { Account, Block, Tools, Wallet } from 'libnemo'\r
-import { assert, env, suite, test } from './GLOBALS.mjs'\r
-import { MAX_RAW, MAX_SUPPLY, NANO_TEST_VECTORS } from './VECTORS.mjs'\r
+import { assert, click, suite, test } from './GLOBALS.mjs'\r
+import { NANO_TEST_VECTORS } from './VECTORS.mjs'\r
 \r
 await Promise.all([\r
 \r
@@ -16,8 +16,16 @@ await Promise.all([
                        await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
                        const account = await wallet.account()\r
                        const data = crypto.randomUUID()\r
-                       const signature = await wallet.sign(0, data)\r
+                       let signature = ''\r
 \r
+                       await assert.resolves(click(\r
+                               'Sign arbitrary string',\r
+                               async () => {\r
+                                       signature = await wallet.sign(0, data)\r
+                               }\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
                        assert.ok(await Tools.verify(account.publicKey, signature, data))\r
                        await assert.resolves(wallet.destroy())\r
                })\r
@@ -28,7 +36,13 @@ await Promise.all([
                        const account = await wallet.account()\r
                        const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou')\r
                                .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000')\r
-                               .sign(wallet, 0)\r
+\r
+                       await assert.resolves(click(\r
+                               'Sign to test subsequent verification success',\r
+                               async () => sendBlock.sign(wallet, 0)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
 \r
                        assert.ok(await sendBlock.verify(account.publicKey))\r
                        await assert.resolves(wallet.destroy())\r
@@ -38,10 +52,16 @@ await Promise.all([
                        const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
                        await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
                        const account = await wallet.account()\r
-\r
                        const sendBlock = await new Block(account.address, '5618869000000000000000000000000', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou')\r
                                .send('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', '2000000000000000000000000000000')\r
-                               .sign(wallet, 0)\r
+\r
+                       await assert.resolves(click(\r
+                               'Sign to test subsequent verification failure',\r
+                               async () => sendBlock.sign(wallet, 0)\r
+                       ))\r
+                       console.log('Click done, waiting 6 seconds to reset transient user activation timer...')\r
+                       await new Promise(r => setTimeout(r, 6000))\r
+\r
                        assert.ok(await sendBlock.verify(account.publicKey))\r
 \r
                        const wrongAccount = new Account('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p')\r