From 85d23b6fc3b1fb4f7890dc7f48c320938ea57251 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sun, 26 Apr 2026 23:32:56 -0700 Subject: [PATCH] Require user activation for Block signing. Update relevant tests. --- src/lib/block.ts | 3 ++ test/test.blocks.mjs | 94 ++++++++++++++++++++++++++++++------ test/test.manage-rolodex.mjs | 30 ++++++++++-- 3 files changed, 106 insertions(+), 21 deletions(-) diff --git a/src/lib/block.ts b/src/lib/block.ts index e74d04c..d57ccc0 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -405,6 +405,9 @@ export class Block { */ async sign (wallet: Wallet, index: number, frontier?: Block): Promise sign (input: unknown, index?: unknown, frontier?: unknown): Block | Promise { + if (navigator.userActivation?.isActive === false) { + throw new Error('Signing request was blocked due to lack of user activation.') + } if (typeof input === 'string' && /^[A-F0-9]{128}$/i.test(input)) { this.signature = input return this diff --git a/test/test.blocks.mjs b/test/test.blocks.mjs index 1b25277..7f1e64f 100644 --- a/test/test.blocks.mjs +++ b/test/test.blocks.mjs @@ -15,11 +15,16 @@ await Promise.all([ await test('generate work automatically when processing block', { skip: isNode }, async () => { const block = await new Block(OPEN_BLOCK.account, '0', OPEN_BLOCK.previous, OPEN_BLOCK.representative) .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance) - .sign(OPEN_BLOCK.key) - assert.nullish(block.work) - const result = await assert.rejects(block.process(rpc)) + await assert.resolves(click( + 'Sign and autogen PoW when processing', + 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.nullish(block.work) + const result = await assert.rejects(block.process(rpc)) assert.exists(block.work) assert.equal(result.message, 'Block could not be processed') }) @@ -207,6 +212,7 @@ await Promise.all([ )) 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) }) @@ -214,7 +220,14 @@ await Promise.all([ await test('sign receive block without work', async () => { const block = new Block(RECEIVE_BLOCK.account, RECEIVE_BLOCK.balance, RECEIVE_BLOCK.previous, RECEIVE_BLOCK.representative) .receive(RECEIVE_BLOCK.link, '0') - await block.sign(RECEIVE_BLOCK.key) + + await assert.resolves(click( + 'Sign receive block without work', + 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) assert.nullish(block.work) @@ -247,7 +260,14 @@ await Promise.all([ const block = await new Block(SEND_BLOCK.account, SEND_BLOCK.balance, SEND_BLOCK.previous, SEND_BLOCK.representative) .send(SEND_BLOCK.link, '0') .pow(SEND_BLOCK.work) - await block.sign(SEND_BLOCK.key) + + await assert.resolves(click( + 'Sign send block with key', + async () => block.sign(SEND_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, SEND_BLOCK.hash) assert.equal(block.signature, SEND_BLOCK.signature) }) @@ -255,7 +275,14 @@ await Promise.all([ await test('sign send block without work', async () => { const block = await new Block(SEND_BLOCK.account, SEND_BLOCK.balance, SEND_BLOCK.previous, SEND_BLOCK.representative) .send(SEND_BLOCK.link, 0n) - .sign(SEND_BLOCK.key) + + await assert.resolves(click( + 'Sign send block without work', + async () => block.sign(SEND_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, SEND_BLOCK.hash) assert.equal(block.signature, SEND_BLOCK.signature) assert.nullish(block.work) @@ -267,7 +294,14 @@ await Promise.all([ const block = await new Block('nano_3igf8hd4sjshoibbbkeitmgkp1o6ug4xads43j6e4gqkj5xk5o83j8ja9php', '3000000000000000000000000000000', '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4') .change('nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs') .pow(work) - await block.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') // Did not find a private key at nano docs for this address + + await assert.resolves(click( + 'Sign change block with key', + async () => block.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') // Did not find a private key at nano docs for this address + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) + assert.equal(block.signature?.toUpperCase(), 'A3C3C66D6519CBC0A198E56855942DEACC6EF741021A1B11279269ADC587DE1DA53CD478B8A47553231104CF24D742E1BB852B0546B87038C19BAE20F9082B0D') assert.equal(block.work, work) }) @@ -275,7 +309,14 @@ await Promise.all([ await test('sign change rep block without work', async () => { const block = await new Block(ADDRESS_0, '0', 'F3C1D7B6EE97DA09D4C00538CEA93CBA5F74D78FD3FBE71347D2DFE7E53DF327') .change('nano_34amtofxstsfyqcgphp8piij9u33widykq9wbz6ysjpxhbgmqu8btu1eexer') - .sign(PRIVATE_0) + + await assert.resolves(click( + 'Sign change block without work', + async () => block.sign(PRIVATE_0) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) + assert.equal(block.signature?.toUpperCase(), '2BD2F905E74B5BEE3E2277CED1D1E3F7535E5286B6E22F7B08A814AA9E5C4E1FEA69B61D60B435ADC2CE756E6EE5F5BE7EC691FE87E024A0B22A3D980CA5B305') assert.nullish(block.work) }) @@ -285,17 +326,38 @@ await Promise.all([ .receive(OPEN_BLOCK.link, OPEN_BLOCK.balance) .pow(OPEN_BLOCK.work) - //@ts-expect-error - await assert.rejects(block.sign()) + await assert.rejects(click( + 'fail to sign without args', + //@ts-expect-error + async () => block.sign() + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.nullish(block.signature) - //@ts-expect-error - await assert.rejects(block.sign(null)) + + await assert.rejects(click( + 'fail to sign with null arg', + //@ts-expect-error + async () => block.sign(null) + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.nullish(block.signature) - //@ts-expect-error - await assert.rejects(block.sign('1')) + + await assert.rejects(click( + 'fail to sign with invalid string length', + async () => block.sign('1') + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.nullish(block.signature) - //@ts-expect-error - await assert.rejects(block.sign('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')) + + await assert.rejects(click( + 'fail to sign with invalid string characters', + async () => block.sign('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) assert.nullish(block.signature) }) }) diff --git a/test/test.manage-rolodex.mjs b/test/test.manage-rolodex.mjs index 40fe37a..5e0de97 100644 --- a/test/test.manage-rolodex.mjs +++ b/test/test.manage-rolodex.mjs @@ -4,7 +4,7 @@ 'use strict' import { Rolodex, Tools } from 'libnemo' -import { assert, suite, test } from './GLOBALS.mjs' +import { assert, click, suite, test } from './GLOBALS.mjs' import { NANO_TEST_VECTORS } from './VECTORS.mjs' await Promise.all([ @@ -183,18 +183,38 @@ await Promise.all([ suite('Rolodex data signature verification', async () => { - await test('should verify valid data and signature', async () => { + await test('verify valid data and signature', async () => { const data = 'Test data' - const signature = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, data) + let signature = '' + + await assert.rejects(click( + 'Sign and then verify Rolodex data/signature', + async () => { + signature = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, data) + } + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) + await assert.resolves(Rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0)) const result = await Rolodex.verify('JohnDoe', signature, data) await assert.resolves(Rolodex.deleteName('JohnDoe')) assert.equal(result, true) }) - await test('should reject incorrect contact for signature', async () => { + await test('reject incorrect contact for signature', async () => { const data = 'Test data' - const signature = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, data) + let signature = '' + + await assert.rejects(click( + 'Sign and then reject invalid Rolodex data/signature', + async () => { + signature = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0 + NANO_TEST_VECTORS.PUBLIC_0, data) + } + )) + console.log('Click done, waiting 6 seconds to reset transient user activation timer...') + await new Promise(r => setTimeout(r, 6000)) + await assert.resolves(Rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_1)) const result = await Rolodex.verify('JaneSmith', signature, data) await assert.resolves(Rolodex.deleteName('JaneSmith')) -- 2.47.3