Rename project for accuracy. Separate async/sync modules. Add tons of tests. Zero out memory when no longer needed. Reduce global static variables by using WASM linear memory directly.
# Authors
-[nano-nacl](https://codecow.com/nano-nacl.git)
+[nano25519](https://codecow.com/nano25519.git)
- Chris Duncan <chris@codecow.com> (codecow.com)
SPDX-License-Identifier: GPL-3.0-or-later
-->
+# Changelog
+
## v0.0.1
### Notable Changes
-Written in Javascript and AssemblyScript, NanoNaCl leverages WebAssembly to sign
-Nano cryptocurrency transaction blocks and validate block signatures.
+Written in TypeScript and AssemblyScript, nano25519 leverages WebAssembly to
+derive public keys, sign transactions, and validate signatures for Nano
+cryptocurrency.
-- Works entirely offline and locally on the device.
+- Fastest available implementation of Ed25519+BLAKE2b required by Nano.
+- Works in both the browser and NodeJS.
+- Optionally import separate modules for fast synchronous functions or
+ non-blocking asynchronous functions.
+- Zero networking. All results are computed entirely offline and locally on the
+ device.
- Zero external dependencies.
- Released under the GPLv3 license.
SPDX-License-Identifier: GPL-3.0-or-later
-->
-# nano-nacl
+# nano25519
_Block signing and validation for Nano cryptocurrency using WebAssembly._
-NanoNaCl is an AssemblyScript implementation of select Ed25519 functions used by
+nano25519 is an AssemblyScript implementation of select Ed25519 functions used by
the Nano cryptocurrency. It is modified to hash using BLAKE2b instead of SHA-512
and can derive public keys from private keys, sign blocks, and verify block
signatures. All processing takes place client-side and offline.
## Installation
```console
-npm i nano-nacl
+npm i nano25519
```
### Browser
-NanoNaCl can be used directly on a webpage with a script module:
+nano25519 can be used directly on a webpage with a script module:
```html
<script type="module">
(async () => {
- const NanoNaCl = await import("https://unpkg.com/nano-nacl@latest");
- const { derive, sign, verify } = NanoNacl;
+ const nano25519 = await import("https://unpkg.com/nano25519@latest");
+ const { derive, sign, verify } = nano25519;
})();
</script>
```
### NodeJS
-NanoNaCl can be imported into NodeJS projects:
+nano25519 can be imported into NodeJS projects:
```javascript
-import { derive, sign, verify } from "nano-nacl";
+import { derive, sign, verify } from "nano25519";
```
## Usage
```javascript
// `prv` is a 32-byte Uint8Array private key
const prv = new Uint8Array(32);
-const pub = NanoNaCl.derive(prvBytes);
+const pub = nano25519.derive(prvBytes);
// `pub` is a 32-byte Uint8Array public key for a Nano account
```
```javascript
// `prv` is a 64-character hex string private key
const prv = "0000000000000000000000000000000000000000000000000000000000000000";
-const pub = await NanoNaCl.deriveAsync(prv);
+const pub = await nano25519.deriveAsync(prv);
// `pub` is a 64-character hex string public key for a Nano account
```
// `prv` is a 32-byte Uint8Array private key
const prv = new Uint8Array(32);
// `pub` is a 32-byte Uint8Array public key derived from `prv`
-const pub = NanoNaCl.derive(prv);
+const pub = nano25519.derive(prv);
// `sk` is a 64-byte Uint8Array secret key joining private and public keys
const sk = new Uint8Array([...prv, ...pub]);
-const sig = NanoNaCl.sign(msg, sk);
+const sig = nano25519.sign(msg, sk);
// `sig` is a 64-byte Uint8Array signature for the block hash
```
// `prv` is a 64-char hex string private key
const prv = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
// `pub` is a 64-char hex string public key derived from `prv`
-const pub = await NanoNaCl.deriveAsync(prv);
+const pub = await nano25519.deriveAsync(prv);
// `sk` is a 128-char hex string secret key joining private and public keys
const sk = prv + pub;
-const sig = await NanoNaCl.signAsync(msg, sk);
+const sig = await nano25519.signAsync(msg, sk);
// `sig` is a 128-char hex string signature for the block hash
```
// `pub` is a 32-byte Uint8Array public key for a Nano account
const pub = new Uint8Array(32);
-const v = NanoNaCl.verify(sig, msg, pub);
+const v = nano25519.verify(sig, msg, pub);
// `v` is a boolean 'true' if the same `prv` that derives `pub` was also used to create `sig` by signing `msg`, else 'false'
```
// `pub` is a 64-char hex string public key for a Nano account
const pub = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
-const v = await NanoNaCl.verifyAsync(sig, msg, pub);
+const v = await nano25519.verifyAsync(sig, msg, pub);
// `v` is a boolean 'true' if the same `prv` that derives `pub` was also used to create `sig` by signing `msg`, else 'false'
```
## Notes
The tweetnacl-js implementation was originally selected as the basis for
-NanoNaCl due to its historical reliability and security. Over time, however, it
+nano25519 due to its historical reliability and security. Over time, however, it
became clear that tweetnacl was designed to optimize size and not speed. Soon
after, libsodium became the reference from which functionality was ported.
1. Compile, minify, and bundle
```console
-git clone https://codecow.com/nano-nacl.git
-cd nano-nacl
+git clone https://codecow.com/nano25519.git
+cd nano25519
npm i
```
## Reporting Bugs
-Email: <bug-nano-nacl@codecow.com>
+Email: <bug-nano25519@codecow.com>
## Acknowledgements
{
"options": {
- "outFile": "./build/nano-nacl.wasm",
- "textFile": "./build/nano-nacl.wat",
+ "outFile": "./build/nano25519.wasm",
+ "textFile": "./build/nano25519.wat",
"optimizeLevel": 3,
"shrinkLevel": 0,
"converge": true,
platform: 'browser',
target: 'esnext',
entryPoints: [
- { in: './index.ts', out: 'browser' }
+ { in: './src/index.ts', out: 'browser' }
],
dropLabels: ['NODE']
}
platform: 'node',
target: 'node22',
entryPoints: [
- { in: './index.ts', out: 'index' }
+ { in: './src/index.ts', out: 'index' },
+ { in: './src/async.ts', out: 'async' },
+ { in: './src/sync.ts', out: 'sync' }
],
packages: 'external',
dropLabels: ['BROWSER']
<script type="module">
await libsodium.ready
await sodium.ready
- let NanoNaCl, NANO_ORG_VECTOR, PYTHON_ED25519_BLAKE2B_VECTORS
+ let nano25519, NANO_ORG_VECTOR, PYTHON_ED25519_BLAKE2B_VECTORS
try {
- NanoNaCl = await import('./dist/browser.js')
+ nano25519 = await import('./dist/browser.js')
} catch (err) {
console.warn(err)
try {
- NanoNaCl = await import('https://unpkg.com/nano-nacl')
+ nano25519 = await import('https://unpkg.com/nano25519')
} catch (err) {
console.warn(err)
try {
- NanoNaCl = await import('https://cdn.jsdelivr.net/npm/nano-nacl@latest')
+ nano25519 = await import('https://cdn.jsdelivr.net/npm/nano25519@latest')
} catch (err) {
- throw new Error(`Failed to load NanoNaCl ${err}`)
+ throw new Error(`Failed to load nano25519 ${err}`)
}
}
}
- window.NanoNaCl = NanoNaCl
+ window.nano25519 = nano25519
try {
({ NANO_ORG_VECTOR, PYTHON_ED25519_BLAKE2B_VECTORS } = await import('./vectors.mjs'))
} catch (err) {
const derive = (sk, api) => {
const sk8 = new Uint8Array(sk.match(/.{2}/g).map(b => parseInt(b, 16)))
switch (api) {
- case 'NanoNaCl': {
- const pk8 = NanoNaCl.derive(sk8)
+ case 'nano25519': {
+ const pk8 = nano25519.derive(sk8)
return [...pk8].map(b => b.toString(16).padStart(2, '0')).join('')
}
- case 'NanoNaCl (async)': {
- return NanoNaCl.deriveAsync(sk)
+ case 'nano25519 (async)': {
+ return nano25519.deriveAsync(sk)
}
case 'NanocurrencyWeb': {
return NanocurrencyWeb.wallet.legacyAccounts(sk)[0].publicKey
const h8 = new Uint8Array(h.match(/.{2}/g).map(b => parseInt(b, 16)))
const sk8 = new Uint8Array((sk + pk).match(/.{2}/g).map(b => parseInt(b, 16)))
switch (api) {
- case 'NanoNaCl': {
- const s8 = NanoNaCl.sign(h8, sk8)
+ case 'nano25519': {
+ const s8 = nano25519.sign(h8, sk8)
return [...s8].map(b => b.toString(16).padStart(2, '0')).join('')
}
- case 'NanoNaCl (async)': {
- return NanoNaCl.signAsync(h, sk + pk)
+ case 'nano25519 (async)': {
+ return nano25519.signAsync(h, sk + pk)
}
case 'NanocurrencyWeb': {
const { privateKey } = NanocurrencyWeb.wallet.legacyAccounts(sk)[0]
const s8 = new Uint8Array(s.match(/.{2}/g).map(b => parseInt(b, 16)))
const pk8 = new Uint8Array(pk.match(/.{2}/g).map(b => parseInt(b, 16)))
switch (api) {
- case 'NanoNaCl': {
- return NanoNaCl.verify(s8, h8, pk8)
+ case 'nano25519': {
+ return nano25519.verify(s8, h8, pk8)
}
- case 'NanoNaCl (async)': {
- return NanoNaCl.verifyAsync(s, h, pk)
+ case 'nano25519 (async)': {
+ return nano25519.verifyAsync(s, h, pk)
}
case 'NanocurrencyWeb': {
return NanocurrencyWeb.tools.verify(pk, s, h)
// self-check
document.getElementById('status').innerHTML = `RUNNING SELF-CHECK`
- console.log(`%cNanoNaCl`, 'color:green', 'Checking validation against known values')
+ console.log(`%cnano25519`, 'color:green', 'Checking validation against known values')
const zeroes = '0000000000000000000000000000000000000000000000000000000000000000'
const expect = []
let result, test, passes = 0, failures = 0
// PASS
- result = await NanoNaCl.deriveAsync(NANO_ORG_VECTOR.privateKey)
+ result = await nano25519.deriveAsync(NANO_ORG_VECTOR.privateKey)
test = result.toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase()
check(`async derive from ${NANO_ORG_VECTOR.privateKey}`, test)
passes += +test
failures += +!test
- result = await NanoNaCl.signAsync(NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.secretKey)
+ result = await nano25519.signAsync(NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.secretKey)
test = result.toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase()
check(`async sign message ${NANO_ORG_VECTOR.blockHash}`, test)
passes += +test
failures += +!test
- result = await NanoNaCl.verifyAsync(NANO_ORG_VECTOR.signature, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey)
+ result = await nano25519.verifyAsync(NANO_ORG_VECTOR.signature, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey)
test = result === true
check(`async verify signature ${NANO_ORG_VECTOR.signature}`, test)
passes += +test
failures += +!test
- result = NanoNaCl.derive(NANO_ORG_VECTOR.privateKeyBytes)
+ result = nano25519.derive(NANO_ORG_VECTOR.privateKeyBytes)
test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase()
check(`derive from ${NANO_ORG_VECTOR.privateKey}`, test)
passes += +test
failures += +!test
- result = NanoNaCl.sign(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes)
+ result = nano25519.sign(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes)
test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase()
check(`sign message ${NANO_ORG_VECTOR.blockHash}`, test)
passes += +test
failures += +!test
- result = NanoNaCl.verify(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes)
+ result = nano25519.verify(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes)
test = result === true
check(`verify signature ${NANO_ORG_VECTOR.signature}`, test)
passes += +test
failures += +!test
+ // test both strings and bytes as input
for (const { privateKey, publicKey, message, signature } of PYTHON_ED25519_BLAKE2B_VECTORS) {
+ result = nano25519.derive(privateKey)
+ test = result.toLowerCase() === publicKey
+ check(`derive from ${privateKey}`, test)
+ passes += +test
+ failures += +!test
+
+ result = nano25519.sign(message, privateKey + publicKey)
+ test = result.toLowerCase() === signature.slice(0, 128)
+ check(`sign message ${message}`, test)
+ passes += +test
+ failures += +!test
+
+ result = nano25519.verify(signature.slice(0, 128), message, publicKey)
+ test = result === true
+ check(`verify signature ${signature.slice(0, 128)}`, test)
+ passes += +test
+ failures += +!test
+
const privateKeyBytes = new Uint8Array(privateKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
const publicKeyBytes = new Uint8Array(publicKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
const secretKeyBytes = new Uint8Array([...privateKeyBytes, ...publicKeyBytes])
const messageBytes = new Uint8Array(message.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
const signatureBytes = new Uint8Array(signature.match(/.{2}/g)?.slice(0, 64).map(b => parseInt(b, 16)) ?? [])
- result = NanoNaCl.derive(privateKeyBytes)
+ result = nano25519.derive(privateKeyBytes)
test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === publicKey
check(`derive from ${privateKey}`, test)
passes += +test
failures += +!test
- result = NanoNaCl.sign(messageBytes, secretKeyBytes)
+ result = nano25519.sign(messageBytes, secretKeyBytes)
test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === signature.slice(0, 128)
check(`sign message ${message}`, test)
passes += +test
failures += +!test
- result = NanoNaCl.verify(signatureBytes, messageBytes, publicKeyBytes)
+ result = nano25519.verify(signatureBytes, messageBytes, publicKeyBytes)
test = result === true
check(`verify signature ${signature.slice(0, 128)}`, test)
passes += +test
}
// XFAIL
- result = await NanoNaCl.verifyAsync(NANO_ORG_VECTOR.badSignature, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey)
+ result = await nano25519.verifyAsync(NANO_ORG_VECTOR.badSignature, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey)
console.log(result)
test = result === false
check(`verify() output for non-canonical signature`, test)
passes += +test
failures += +!test
- result = await NanoNaCl.verifyAsync(NANO_ORG_VECTOR.signature, random(), NANO_ORG_VECTOR.publicKey)
+ result = await nano25519.verifyAsync(NANO_ORG_VECTOR.signature, random(), NANO_ORG_VECTOR.publicKey)
console.log(result)
test = result === false
check(`verify() output for random block hash`, test)
passes += +test
failures += +!test
- result = await NanoNaCl.verifyAsync(NANO_ORG_VECTOR.signature, zeroes, NANO_ORG_VECTOR.publicKey)
+ result = await nano25519.verifyAsync(NANO_ORG_VECTOR.signature, zeroes, NANO_ORG_VECTOR.publicKey)
console.log(result)
test = result === false
check(`verify() output for bad block hash`, test)
passes += +test
failures += +!test
- result = await NanoNaCl.verifyAsync(`${zeroes}${zeroes}`, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey)
+ result = await nano25519.verifyAsync(`${zeroes}${zeroes}`, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey)
console.log(result)
test = result === false
check(`verify() output for bad signature`, test)
passes += +test
failures += +!test
- result = await NanoNaCl.verifyAsync(NANO_ORG_VECTOR.signature, NANO_ORG_VECTOR.blockHash, zeroes)
+ result = await nano25519.verifyAsync(NANO_ORG_VECTOR.signature, NANO_ORG_VECTOR.blockHash, zeroes)
console.log(result)
test = result === false
check(`verify() output for bad public key`, test)
const validation = document.getElementById('validation')
validation.innerText = '⏳'
if (signature.length === 128 && blockHash.length === 64 && publicKey.length === 64) {
- NanoNaCl.verify(signature, blockHash, publicKey)
+ nano25519.verify(signature, blockHash, publicKey)
.then(result => {
validation.innerText = result
? '✔️'
</head>
<body>
- <h1>nano-nacl</h1>
- <h4><a href="https://www.npmjs.com/package/nano-nacl">https://www.npmjs.com/package/nano-nacl</a></h4>
- <h2>Speed test for NanoNaCl tool.</h2>
+ <h1>nano25519</h1>
+ <h4><a href="https://www.npmjs.com/package/nano25519">https://www.npmjs.com/package/nano25519</a></h4>
+ <h2>Speed test for nano25519</h2>
<p>Times below are in milliseconds and are summarized by various averaging methods.</p>
<hr />
<label for="signature">Verify Signature</label>
<span>
<label for="api">API</label>
<select id="api">
- <option>NanoNaCl</option>
- <option>NanoNaCl (async)</option>
+ <option>nano25519</option>
+ <option>nano25519 (async)</option>
<option>NanocurrencyWeb</option>
<option>Sodium</option>
<option>TweetNaCl.js</option>
{
- "name": "nano-nacl",
+ "name": "nano25519",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "nano-nacl",
+ "name": "nano25519",
"version": "0.0.1",
"license": "(GPL-3.0-or-later AND MIT)",
"devDependencies": {
{
- "name": "nano-nacl",
+ "name": "nano25519",
"version": "0.0.1",
"description": "Nano cryptocurrency public key derivation, transaction signing, and signature verification using AssemblyScript.",
"keywords": [
"wasm"
],
"homepage": "https://git.codecow.com",
- "bugs": "bug-nano-nacl@codecow.com",
+ "bugs": "bug-nano25519@codecow.com",
"license": "(GPL-3.0-or-later AND MIT)",
"author": "Chris Duncan <chris@codecow.com>",
"funding": {
],
"repository": {
"type": "git",
- "url": "git+https://codecow.com/nano-nacl.git"
+ "url": "git+https://codecow.com/nano25519.git"
},
"scripts": {
"build": "npm run clean && npm run compile && node ./esbuild/dev.mjs",
"build:prod": "npm run clean && npm run compile && node ./esbuild/prod.mjs",
"clean": "rm -rf {build,dist}",
- "compile": "asc ./assembly/index.ts && tsc",
+ "compile": "asc ./src/assembly/index.ts && tsc",
"prepublishOnly": "npm run test:prod",
"test": "npm run build && node ./test.mjs",
"test:prod": "npm run build:prod && node ./test.mjs"
"types": "./dist/index.d.ts",
"node": "./dist/index.js",
"import": "./dist/browser.js"
+ },
+ "./sync": {
+ "types": "./dist/sync.d.ts",
+ "node": "./dist/sync.js"
+ },
+ "./async": {
+ "types": "./dist/async.d.ts",
+ "node": "./dist/async.js"
}
},
"browser": {
t: v128 = v128.splat<u64>(0)
v: StaticArray<u64> = new StaticArray<u64>(16)
- init (): Blake2b {
- // initialize buffers and counters
+ @inline
+ clear (): void {
this.b.fill(0)
this.c = 0
+ this.h.fill(0)
this.m.fill(0)
+ this.p.fill(0)
this.t = v128.splat<u64>(0)
this.v.fill(0)
+ }
+
+ init (): Blake2b {
+ // reset buffers and counters
+ this.clear()
// initialize parameter block
- this.p.fill(0)
this.p[0] = 64 // always 64 bytes for this implementation
this.p[1] = 0 // no key
this.p[2] = 1 // fanout
// return byte array of 64-byte chain buffer
memory.copy(changetype<usize>(output), changetype<usize>(this.h), 64)
+
+ // clear internal buffers and counters
+ this.clear()
}
// Defined in BLAKE2 section 2.4
k[31] |= 64\r
}\r
\r
-function crypto_derive (pk: StaticArray<u8>, sk: StaticArray<u8>, seed: StaticArray<u8>): void {\r
- blake2b.init().update(changetype<usize>(seed), seed.length).digest(sk)\r
+const crypto_derive_pk: StaticArray<u8> = new StaticArray<u8>(PUBLICKEY_BYTES)\r
+function crypto_derive (sk: StaticArray<u8>, seed: StaticArray<u8>): void {\r
+ const pk = crypto_derive_pk\r
+ blake2b.init().update(changetype<usize>(seed), 32).digest(sk)\r
clamp(sk)\r
\r
ge_scalarmult_base_tobytes(pk, sk)\r
const crypto_sign_az = new StaticArray<u8>(64)\r
const crypto_sign_nonce = new StaticArray<u8>(64)\r
const crypto_sign_hram = new StaticArray<u8>(64)\r
-const crypto_sign_zm = new StaticArray<u8>(SIGNATURE_BYTES)\r
const crypto_sign_t = new StaticArray<u8>(PRIVATEKEY_BYTES)\r
function crypto_sign (s: StaticArray<u8>, m: usize, mlen: i32, sk: StaticArray<u8>): void {\r
const az = crypto_sign_az\r
const nonce = crypto_sign_nonce\r
const hram = crypto_sign_hram\r
- const zm = changetype<usize>(crypto_sign_zm)\r
const t = changetype<usize>(crypto_sign_t)\r
\r
// Hash secret key to private scalar `a` and prefix for nonce derivation `z`\r
sc_reduce(hram)\r
\r
// Compute `S = (r + h*a) mod L` and construct final signature `s = (R || S)`\r
- sc_muladd(changetype<StaticArray<u8>>(changetype<usize>(s) + 32), az, hram, nonce)\r
+ sc_muladd(changetype<StaticArray<u8>>(changetype<usize>(s) + 32), changetype<StaticArray<u8>>(az), hram, nonce)\r
\r
// Clean up sensitive data\r
az.fill(0)\r
+ hram.fill(0)\r
nonce.fill(0)\r
+ sk.fill(0)\r
+ memory.fill(t, 0, PRIVATEKEY_BYTES)\r
}\r
\r
const crypto_verify_h = new StaticArray<u8>(64)\r
return MESSAGE_BUFFER\r
}\r
\r
-const derive_pk = new StaticArray<u8>(PUBLICKEY_BYTES)\r
-const derive_sk = new StaticArray<u8>(SECRETKEY_BYTES)\r
-const derive_seed = new StaticArray<u8>(PRIVATEKEY_BYTES)\r
/**\r
* Derives a Nano public key from a private key and writes it to the static\r
* output buffer. This mirrors the functionality of\r
- * `nacl.sign.keyPair.fromSeed()`, returning just the `publicKey` property.\r
+ * `nacl.sign.keyPair.fromSeed()`, returning both private and public keys as a\r
+ * concatenated `secretKey` property.\r
*/\r
export function derive (): void {\r
- const pk = derive_pk\r
- const sk = derive_sk\r
- const seed = derive_seed\r
-\r
- for (let i = 0; i < PRIVATEKEY_BYTES; i++) {\r
- seed[i] = load<u8>(INPUT_BUFFER + i)\r
- }\r
-\r
- crypto_derive(pk, sk, seed)\r
- for (let i = 0; i < PUBLICKEY_BYTES; i++) {\r
- store<u8>(OUTPUT_BUFFER + i, pk[i])\r
- }\r
+ const sk = changetype<StaticArray<u8>>(OUTPUT_BUFFER)\r
+ const seed = changetype<StaticArray<u8>>(INPUT_BUFFER)\r
+ memory.fill(OUTPUT_BUFFER, 0, SECRETKEY_BYTES)\r
+ crypto_derive(sk, seed)\r
+ memory.fill(INPUT_BUFFER, 0, PRIVATEKEY_BYTES)\r
}\r
\r
-const sign_sk = new StaticArray<u8>(SECRETKEY_BYTES)\r
const sign_s = new StaticArray<u8>(SIGNEDMESSAGE_BYTES)\r
/**\r
* Sign a message using a private key. The signature is written to the static\r
* @param {u64} sk - 64-byte secret key (32-byte private key + 32-byte public key)\r
*/\r
export function sign (mlen: i32): void {\r
+ if (mlen < 0 || mlen > 32768) throw new Error()\r
const s = sign_s\r
const m = MESSAGE_BUFFER\r
- const sk = sign_sk\r
+ const sk = changetype<StaticArray<u8>>(INPUT_BUFFER)\r
\r
- memory.copy(changetype<usize>(sk), INPUT_BUFFER, SECRETKEY_BYTES)\r
+ memory.fill(OUTPUT_BUFFER, 0, SIGNATURE_BYTES)\r
crypto_sign(s, m, mlen, sk)\r
memory.copy(OUTPUT_BUFFER, changetype<usize>(s), SIGNATURE_BYTES)\r
+ memory.fill(INPUT_BUFFER, 0, SECRETKEY_BYTES)\r
}\r
\r
const verify_s = new StaticArray<u8>(SIGNATURE_BYTES)\r
* @param {u64} k - 32-byte public key\r
*/\r
export function verify (mlen: i32): void {\r
+ if (mlen < 0 || mlen > 32768) throw new Error()\r
const s = verify_s\r
const m = MESSAGE_BUFFER\r
const k = verify_k\r
\r
- let ptr = INPUT_BUFFER\r
- for (let i = 0; i < SIGNATURE_BYTES; i++) {\r
- s[i] = load<u8>(usize(i) + ptr)\r
- }\r
- ptr = INPUT_BUFFER + SIGNATURE_BYTES\r
- for (let i = 0; i < PUBLICKEY_BYTES; i++) {\r
- k[i] = load<u8>(usize(i) + ptr)\r
- }\r
-\r
+ memory.fill(OUTPUT_BUFFER, 0, SIGNATURE_BYTES)\r
+ memory.copy(changetype<usize>(s), INPUT_BUFFER, SIGNATURE_BYTES)\r
+ memory.copy(changetype<usize>(k), INPUT_BUFFER + SIGNATURE_BYTES, PUBLICKEY_BYTES)\r
const v = crypto_verify(s, m, mlen, k)\r
store<u8>(OUTPUT_BUFFER, v)\r
+ memory.fill(INPUT_BUFFER, 0, SIGNATURE_BYTES)\r
}\r
--- /dev/null
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { run } from './nano25519'
+
+/**
+ * Asynchronous Nano public key derivation using WebAssembly.
+ * @param {Uint8Array<ArrayBuffer>} k - 64-character private key hex string
+ * @returns Promise for 64-character public key hex string
+ */
+export async function deriveAsync (privateKey: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>
+/**
+ * Asynchronous Nano public key derivation using WebAssembly.
+ * @param {string} k - 64-character private key hex string
+ * @returns Promise for 64-character public key hex string
+ */
+export async function deriveAsync (privateKey: string): Promise<string>
+export async function deriveAsync (privateKey: string | Uint8Array<ArrayBuffer>): Promise<string | Uint8Array<ArrayBuffer>> {
+ return run({ action: 'derive', privateKey })
+}
+
+/**
+ * Asynchronous signing using WebAssembly. To sign Nano blocks, the message
+ * should be a 64-character block hash.
+ * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string up to 65536 characters
+ * @param {Uint8Array<ArrayBuffer>} k - 128-character secret key (prv + pub) hex string
+ * @returns Promise for 128-character detached signature hex string
+ */
+export async function signAsync (message: Uint8Array<ArrayBuffer>, secretKey: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>
+/**
+ * Asynchronous signing using WebAssembly. To sign Nano blocks, the message
+ * should be a 64-character block hash.
+ * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string up to 65536 characters
+ * @param {Uint8Array<ArrayBuffer>} k - 128-character secret key (prv + pub) hex string
+ * @returns Promise for 128-character detached signature hex string
+ */
+export async function signAsync (message: string, secretKey: string): Promise<string>
+export async function signAsync (message: string | Uint8Array<ArrayBuffer>, secretKey: string | Uint8Array<ArrayBuffer>): Promise<string | Uint8Array<ArrayBuffer>> {
+ return run({ action: 'sign', message, secretKey })
+}
+
+/**
+ * Asynchronous signature verification using WebAssembly. To verify Nano block
+ * signatures, the message should be a 64-character block hash.
+ * @param {Uint8Array<ArrayBuffer>} s - 128-character detached signature hex string
+ * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string to up 65536 characters
+ * @param {Uint8Array<ArrayBuffer>} k - 64-character public key hex string
+ * @returns Promise resolving to true if signature matches block hash and public key, else false
+ */
+export async function verifyAsync (signature: Uint8Array<ArrayBuffer>, message: Uint8Array<ArrayBuffer>, publicKey: Uint8Array<ArrayBuffer>): Promise<boolean>
+/**
+ * Asynchronous signature verification using WebAssembly. To verify Nano block
+ * signatures, the message should be a 64-character block hash.
+ * @param {Uint8Array<ArrayBuffer>} s - 128-character detached signature hex string
+ * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string to up 65536 characters
+ * @param {Uint8Array<ArrayBuffer>} k - 64-character public key hex string
+ * @returns Promise resolving to true if signature matches block hash and public key, else false
+ */
+export async function verifyAsync (signature: string, message: string, publicKey: string): Promise<boolean>
+export async function verifyAsync (signature: string | Uint8Array<ArrayBuffer>, message: string | Uint8Array<ArrayBuffer>, publicKey: string | Uint8Array<ArrayBuffer>): Promise<boolean> {
+ return !!(await run({ action: 'verify', signature, message, publicKey }))
+}
--- /dev/null
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+export { deriveAsync, signAsync, verifyAsync } from './async'
+export { derive, sign, verify } from './sync'
+
import { Worker as NodeWorker } from 'node:worker_threads'
//@ts-expect-error
-import nacl from './build/nano-nacl.wasm'
+import nano25519_wasm from '../build/nano25519.wasm'
type Data = {
action: string
}
}
-const startNanoNaCl = (bytes: number[]): { derive: typeof derive, sign: typeof sign, verify: typeof verify } => {
+const nano25519_init = (bytes: number[]): { derive: typeof derive, sign: typeof sign, verify: typeof verify } => {
const wasm: Uint8Array<ArrayBuffer> = Uint8Array.from(bytes)
const module = new WebAssembly.Module(wasm)
const { exports } = new WebAssembly.Instance(module, {
env: {
abort: (msg: any, file: any, row: any, col: any) => {
// ~lib/builtins/abort(~lib/string/String | null?, ~lib/string/String | null?, u32?, u32?) => void
- // msg = __liftString(msg >>> 0)
- // file = __liftString(file >>> 0)
+ msg = msg >>> 0
+ file = file >>> 0
row = row >>> 0
col = col >>> 0
console.error('wasm abort:', `msg ${msg}`, `file ${file}`, `row ${row}`, `col ${col}`)
console.log(message, ...[a0, a1, a2, a3, a4].slice(0, n))
})()
},
- memory: new WebAssembly.Memory({ initial: 4, maximum: 4 })
+ memory: new WebAssembly.Memory({ initial: 1, maximum: 1 })
}
}) as Exports
- function derive (k: unknown): Uint8Array<ArrayBuffer> {
- validate('private key', k, 32)
+ function derive (k: unknown): string | Uint8Array<ArrayBuffer> {
+ const privateKey = normalize('private key', 32, 32, k)
let buffer: DataView | undefined = new DataView(exports.memory.buffer)
let inPtr = exports.getInputPointer()
for (let i = 0; i < 32; i++) {
- buffer.setUint8(inPtr + i, k[i])
+ buffer.setUint8(inPtr + i, privateKey[i])
}
exports.derive()
const outPtr = exports.getOutputPointer()
- const pk = new Uint8Array(32)
+ const sk = new Uint8Array(32)
buffer = new DataView(exports.memory.buffer)
for (let i = 0; i < 32; i++) {
- pk[i] = buffer.getUint8(outPtr + i)
+ sk[i] = buffer.getUint8(outPtr + i + 32)
+ buffer.setUint8(outPtr + i, 0)
+ buffer.setUint8(outPtr + i + 32, 0)
}
buffer = undefined
- return pk
+ return typeof k === 'string'
+ ? [...sk].map(b => b.toString(16).padStart(2, '0')).join('')
+ : sk
}
- function sign (m: unknown, k: unknown): Uint8Array<ArrayBuffer> {
- validate('message', m)
- validate('secret key', k, 64)
+ function sign (m: unknown, k: unknown): string | Uint8Array<ArrayBuffer> {
+ const message = normalize('message', 0, 32768, m)
+ const secretKey = normalize('secret key', 64, 64, k)
let buffer: DataView | undefined = new DataView(exports.memory.buffer)
let mPtr = exports.getMessagePointer()
let inPtr = exports.getInputPointer()
- for (let i = 0; i < m.byteLength; i++) {
- buffer.setUint8(mPtr + i, m[i])
+ for (let i = 0; i < message.byteLength; i++) {
+ buffer.setUint8(mPtr + i, message[i])
}
for (let i = 0; i < 64; i++) {
- buffer.setUint8(inPtr + i, k[i])
+ buffer.setUint8(inPtr + i, secretKey[i])
}
- exports.sign(m.byteLength)
+ exports.sign(message.byteLength)
const outPtr = exports.getOutputPointer()
const s = new Uint8Array(64)
buffer = new DataView(exports.memory.buffer)
for (let i = 0; i < 64; i++) {
s[i] = buffer.getUint8(outPtr + i)
+ buffer.setUint8(outPtr + i, 0)
}
buffer = undefined
- return s
+ return typeof k === 'string'
+ ? [...s].map(b => b.toString(16).padStart(2, '0')).join('')
+ : s
}
- function verify (s: unknown, m: unknown, k: unknown): Uint8Array<ArrayBuffer> {
- validate('signature', s, 64)
- validate('message', m)
- validate('public key', k, 32)
+ function verify (s: unknown, m: unknown, k: unknown): boolean {
+ const signature = normalize('signature', 64, 64, s)
+ const message = normalize('message', 0, 32768, m)
+ const publicKey = normalize('public key', 32, 32, k)
let buffer: DataView | undefined = new DataView(exports.memory.buffer)
let mPtr = exports.getMessagePointer()
let inPtr = exports.getInputPointer()
- for (let i = 0; i < m.byteLength; i++) {
- buffer.setUint8(mPtr + i, m[i])
+ for (let i = 0; i < message.byteLength; i++) {
+ buffer.setUint8(mPtr + i, message[i])
}
for (let i = 0; i < 64; i++) {
- buffer.setUint8(inPtr + i, s[i])
+ buffer.setUint8(inPtr + i, signature[i])
}
inPtr += 64
for (let i = 0; i < 32; i++) {
- buffer.setUint8(inPtr + i, k[i])
+ buffer.setUint8(inPtr + i, publicKey[i])
}
- exports.verify(m.byteLength)
+ exports.verify(message.byteLength)
const outPtr = exports.getOutputPointer()
const v = new Uint8Array(1)
buffer = new DataView(exports.memory.buffer)
v[0] = buffer.getUint8(outPtr)
+ for (let i = 0; i < 64; i++) {
+ buffer.setUint8(outPtr + i, 0)
+ }
buffer = undefined
- return v
+ return v[0] === 0
}
- function toBytes (type: string, byteLength: number, value?: unknown): Uint8Array<ArrayBuffer> {
- if (value instanceof ArrayBuffer) {
- return new Uint8Array(value)
+ function normalize (name: string, byteLengthMin: number, byteLengthMax: number, value: unknown): Uint8Array<ArrayBuffer> {
+ if (typeof name !== 'string') {
+ throw new TypeError(`Invalid name ${name}`)
}
- if (typeof type !== 'string') {
- throw new TypeError(`toBytes(): Invalid type ${type}`)
+ if (typeof byteLengthMin !== 'number' || typeof byteLengthMax !== 'number') {
+ throw new TypeError(`Invalid byte length for ${name}`)
}
- if (typeof byteLength !== 'number') {
- throw new TypeError(`toBytes(): Invalid byte length ${byteLength}`)
+ if (typeof value === 'string') {
+ const regex = RegExp(`[A-Fa-f0-9]{${byteLengthMin << 1},${byteLengthMax << 1}}`)
+ if (!regex.test(value)) {
+ throw new TypeError(`Invalid ${name} ${value}`)
+ }
+ value = new Uint8Array(value.match(/[A-Fa-f0-9]{2}/g)?.map(b => parseInt(b, 16)) || [])
}
- const regex = RegExp(`[a-z0-9]{${byteLength << 1}}`, 'i')
- if (typeof value !== 'string' || !regex.test(value)) {
- throw new TypeError(`toBytes(): Invalid ${type} ${value}`)
+ if (value instanceof ArrayBuffer) {
+ value = new Uint8Array(value)
}
- const bytes = new Uint8Array(value.match(/.{2}/g)?.map(b => parseInt(b, 16)) || [])
- if (bytes.byteLength !== byteLength) {
- throw new TypeError(`toBytes(): Error parsing ${type} ${bytes}`)
+ if (!(value instanceof Uint8Array)) {
+ throw new TypeError(`${name} must be Uint8Array`)
}
- return bytes
- }
-
- function validate (type: string, bytes?: unknown, byteLength?: number): asserts bytes is Uint8Array<ArrayBuffer> {
- if (!(bytes instanceof Uint8Array)) {
- throw new TypeError(`${type} must be Uint8Array`)
+ if (!('buffer' in value && value.buffer instanceof ArrayBuffer)) {
+ throw new TypeError(`${name} must be backed by an ArrayBuffer`)
}
- if (!('buffer' in bytes)) {
- throw new TypeError(`${type} must be backed by an ArrayBuffer`)
+ if (value.byteLength < byteLengthMin) {
+ throw new TypeError(`${name} must be at least ${byteLengthMin} bytes`)
}
- if (byteLength != null && bytes.buffer.byteLength !== byteLength) {
- throw new TypeError(`${type} must be ${byteLength} bytes`)
+ if (value.byteLength > byteLengthMax) {
+ throw new TypeError(`${name} must be no more than ${byteLengthMax} bytes`)
}
+ return value as Uint8Array<ArrayBuffer>
}
let isListening = false
let host: any = null
/**
- * Parses inbound data when NanoNaCl is started as a Web Worker.
+ * Parses inbound data when nano25519 is started as a Web Worker. Only called
+ * by functions in `async` module.
* @param {object} message.data - Worker commands and related data
*/
function handleMessage (message: unknown): void {
|| !('action' in message.data)
|| typeof message.data.action !== 'string'
) {
- throw new TypeError('Invalid NanoNaClWorker request')
+ throw new TypeError('Invalid nano25519Worker request')
}
const data: Data = message.data as object & Record<"action", string>
if (data.action === 'start') {
const { action } = data
if (action === 'derive') {
const { privateKey } = data
- const privateKeyBytes = toBytes('private key', 32, privateKey)
- const publicKeyBytes = derive(privateKeyBytes)
- if (publicKeyBytes == null || !(publicKeyBytes instanceof Uint8Array) || publicKeyBytes.byteLength != 32) {
+ const publicKeyBytes = derive(privateKey)
+ if (publicKeyBytes == null) {
throw new TypeError('Invalid public key from WASM derive()')
}
result = publicKeyBytes
} else if (action === 'sign') {
const { message, secretKey } = data
- const messageBytes = toBytes('message', typeof message === 'string' ? message.length >> 1 : 0, message)
- const secretKeyBytes = toBytes('secret key', 64, secretKey)
- const signature = sign(messageBytes, secretKeyBytes)
+ const signature = sign(message, secretKey)
if (signature == null) {
throw new TypeError('Invalid signature from WASM sign()')
}
result = signature
} else if (action === 'verify') {
const { message, publicKey, signature } = data
- const signatureBytes = toBytes('signature', 64, signature)
- const messageBytes = toBytes('message', typeof message === 'string' ? message.length >> 1 : 0, message)
- const publicKeyBytes = toBytes('public key', 32, publicKey)
- const verification = verify(signatureBytes, messageBytes, publicKeyBytes)
+ const verification = verify(signature, message, publicKey)
if (verification == null) {
throw new TypeError('Invalid verification from WASM verify()')
}
}
}
} catch (err: unknown) {
+ let buffer: DataView | undefined = new DataView(exports.memory.buffer)
+ let inPtr = exports.getInputPointer()
+ for (let i = 0; i < 128; i++) {
+ buffer.setUint8(inPtr + i, 0)
+ }
if (typeof err === 'object' && err != null) {
const e = err as { [k: string]: unknown }
if (e.message !== 'divide by zero') {
return { derive, sign, verify }
}
+export const nano25519 = nano25519_init(nano25519_wasm)
+
+const nano25519_worker = `;(${nano25519_init})([${nano25519_wasm}]);`
+
/**
* Host code for asynchronous Web Worker
*/
// Create worker module
function init (): void {
try {
- url = URL.createObjectURL(new Blob([NanoNaClWorker], { type: 'text/javascript' }))
+ url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' }))
BROWSER: worker = new Worker(url, { type: 'module' })
- NODE: worker = new NodeWorker(NanoNaClWorker, {
+ NODE: worker = new NodeWorker(nano25519_worker, {
eval: true,
stderr: false,
stdout: false
})
- console.log(`NanoNaCl initialized.`)
+ console.log(`nano25519 initialized.`)
isWorkerReady = true
} catch (err) {
isWorkerReady = false
- throw new Error('NanoNaCl initialization failed.', { cause: err })
+ throw new Error('nano25519 initialization failed.', { cause: err })
}
}
// Reconstruct worker when errors occur
function reset (): void {
- console.warn(`NanoNaCl encountered an error. Reinitializing...`)
+ console.warn(`nano25519 encountered an error. Reinitializing...`)
isWorkerReady = false
worker.terminate()
init()
}
}
-// Send command and relevant data to NanoNaCl worker
-async function dispatch (data: { [key: string]: string | Uint8Array<ArrayBuffer> }): Promise<Uint8Array<ArrayBuffer>> {
+// Send command and relevant data to nano25519 worker
+async function dispatch (data: { [key: string]: string | ArrayBuffer | Uint8Array<ArrayBuffer> }): Promise<Uint8Array<ArrayBuffer>> {
return new Promise((resolve, reject) => {
- const transfer: ArrayBuffer[] = Object.values(data).filter(v => v instanceof Uint8Array).map(({ buffer }) => buffer)
+ const transfer: ArrayBuffer[] = []
+ for (let k of Object.keys(data)) {
+ if (data[k] instanceof Uint8Array) {
+ data[k] = data[k].buffer.slice()
+ transfer.push(data[k])
+ }
+ }
const onresult = (msg: any): void => {
const result = msg.data
console.log(`received result from worker: `, result)
})
}
-async function run (data: Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string> {
+export async function run (data: Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>> {
try {
await start()
} catch (err: any) {
}
try {
const result = await dispatch(data)
- if (!(result instanceof Uint8Array)) {
+ if (typeof result !== 'boolean' && typeof result !== 'string' && !(result instanceof Uint8Array)) {
throw new Error(result)
}
- return [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase()
+ return result
} catch (err: any) {
try {
console.error(err)
+ console.error(data)
await stop()
} catch (e: any) {
console.error('failed to stop worker', err?.message ?? err ?? 'unknown reason')
}
}
-const NanoNaCl = startNanoNaCl(nacl)
-const NanoNaClWorker = `;(${startNanoNaCl})([${nacl}]);`
-
-/**
- * Nano public key derivation using WebAssembly.
- * @param {Uint8Array<ArrayBuffer>} k - 32-byte private key
- * @returns 32-byte public key
- */
-export function derive (k: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {
- return NanoNaCl.derive(k)
-}
-
-/**
- * Signing using WebAssembly. To sign Nano blocks, the message should be a
- * 32-byte block hash.
- * @param {Uint8Array<ArrayBuffer>} m - Variable-byte-length message up to 32 KiB
- * @param {Uint8Array<ArrayBuffer>} k - 64-byte secret key (prv + pub)
- * @returns 64-byte detached signature
- */
-export function sign (m: Uint8Array<ArrayBuffer>, k: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {
- return NanoNaCl.sign(m, k)
-}
-
/**
- * Signature verification using WebAssembly. To verify Nano block signatures,
- * the message should be a 64-character block hash.
- * @param {Uint8Array<ArrayBuffer>} s - 64-byte detached signature
- * @param {Uint8Array<ArrayBuffer>} m - Variable-byte-length message up to 32 KiB
- * @param {Uint8Array<ArrayBuffer>} k - 32-byte public key
- * @returns true if signature matches block hash and public key, else false
+ * Asynchronous Nano public key derivation using WebAssembly.
+ * @param {Uint8Array<ArrayBuffer>} k - 64-character private key hex string
+ * @returns Promise for 64-character public key hex string
*/
-export function verify (s: Uint8Array<ArrayBuffer>, m: Uint8Array<ArrayBuffer>, k: Uint8Array<ArrayBuffer>): boolean {
- return NanoNaCl.verify(s, m, k)[0] === 0
-}
-
+export async function deriveAsync (privateKey: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>
/**
* Asynchronous Nano public key derivation using WebAssembly.
- * @param {Uint8Array<ArrayBuffer>} k - 64-character private key hex string
+ * @param {string} k - 64-character private key hex string
* @returns Promise for 64-character public key hex string
*/
-export async function deriveAsync (privateKey: string): Promise<string> {
- return await run({ action: 'derive', privateKey })
+export async function deriveAsync (privateKey: string): Promise<string>
+export async function deriveAsync (privateKey: string | Uint8Array<ArrayBuffer>): Promise<string | Uint8Array<ArrayBuffer>> {
+ return run({ action: 'derive', privateKey })
}
/**
* @param {Uint8Array<ArrayBuffer>} k - 128-character secret key (prv + pub) hex string
* @returns Promise for 128-character detached signature hex string
*/
-export const signAsync = async function (message: string, secretKey: string): Promise<string> {
- return await run({ action: 'sign', message, secretKey })
+export async function signAsync (message: Uint8Array<ArrayBuffer>, secretKey: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>
+/**
+ * Asynchronous signing using WebAssembly. To sign Nano blocks, the message
+ * should be a 64-character block hash.
+ * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string up to 65536 characters
+ * @param {Uint8Array<ArrayBuffer>} k - 128-character secret key (prv + pub) hex string
+ * @returns Promise for 128-character detached signature hex string
+ */
+export async function signAsync (message: string, secretKey: string): Promise<string>
+export async function signAsync (message: string | Uint8Array<ArrayBuffer>, secretKey: string | Uint8Array<ArrayBuffer>): Promise<string | Uint8Array<ArrayBuffer>> {
+ return run({ action: 'sign', message, secretKey })
}
/**
* @param {Uint8Array<ArrayBuffer>} k - 64-character public key hex string
* @returns Promise resolving to true if signature matches block hash and public key, else false
*/
-export const verifyAsync = async function (signature: string, message: string, publicKey: string): Promise<boolean> {
- return await run({ action: 'verify', signature, message, publicKey }) === '00'
+export async function verifyAsync (signature: Uint8Array<ArrayBuffer>, message: Uint8Array<ArrayBuffer>, publicKey: Uint8Array<ArrayBuffer>): Promise<boolean>
+/**
+ * Asynchronous signature verification using WebAssembly. To verify Nano block
+ * signatures, the message should be a 64-character block hash.
+ * @param {Uint8Array<ArrayBuffer>} s - 128-character detached signature hex string
+ * @param {Uint8Array<ArrayBuffer>} m - Variable-length message hex string to up 65536 characters
+ * @param {Uint8Array<ArrayBuffer>} k - 64-character public key hex string
+ * @returns Promise resolving to true if signature matches block hash and public key, else false
+ */
+export async function verifyAsync (signature: string, message: string, publicKey: string): Promise<boolean>
+export async function verifyAsync (signature: string | Uint8Array<ArrayBuffer>, message: string | Uint8Array<ArrayBuffer>, publicKey: string | Uint8Array<ArrayBuffer>): Promise<boolean> {
+ return !!(await run({ action: 'verify', signature, message, publicKey }))
}
--- /dev/null
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { nano25519 } from "./nano25519"
+
+/**
+ * Nano public key derivation using WebAssembly.
+ * @param {string} k - 64-character hexadecimal private key
+ * @returns 64-character hexadecimal public key
+ */
+export function derive (k: string): string
+/**
+ * Nano public key derivation using WebAssembly.
+ * @param {Uint8Array<ArrayBuffer>} k - 32-byte private key
+ * @returns 32-byte public key
+ */
+export function derive (k: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>
+export function derive (k: string | Uint8Array<ArrayBuffer>): string | Uint8Array<ArrayBuffer> {
+ return nano25519.derive(k)
+}
+
+/**
+ * Signing using WebAssembly. To sign Nano blocks, the message should be a
+ * 64-character hexadecimal block hash.
+ * @param {string} m - Variable-length hexadecimal message up to 32 KiB
+ * @param {string} k - 128-character hexadecimal secret key (prv + pub)
+ * @returns 128-character hexadecimal detached signature
+ */
+export function sign (m: string, k: string): string
+/**
+ * Signing using WebAssembly. To sign Nano blocks, the message should be a
+ * 32-byte block hash.
+ * @param {Uint8Array<ArrayBuffer>} m - Variable-byte-length message up to 32 KiB
+ * @param {Uint8Array<ArrayBuffer>} k - 64-byte secret key (prv + pub)
+ * @returns 64-byte detached signature
+ */
+export function sign (m: Uint8Array<ArrayBuffer>, k: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>
+export function sign (m: string | Uint8Array<ArrayBuffer>, k: string | Uint8Array<ArrayBuffer>): string | Uint8Array<ArrayBuffer> {
+ return nano25519.sign(m, k)
+}
+
+/**
+ * Signature verification using WebAssembly. To verify Nano block signatures,
+ * the message should be a 64-character block hash.
+ * @param {string} s - 128-character hexadecimal detached signature
+ * @param {string} m - Variable-length message up to 32 KiB
+ * @param {string} k - 64-character hexadecimal public key
+ * @returns true if signature matches block hash and public key, else false
+ */
+export function verify (s: string, m: string, k: string): boolean
+/**
+ * Signature verification using WebAssembly. To verify Nano block signatures,
+ * the message should be a 64-character block hash.
+ * @param {Uint8Array<ArrayBuffer>} s - 64-byte detached signature
+ * @param {Uint8Array<ArrayBuffer>} m - Variable-byte-length message up to 32 KiB
+ * @param {Uint8Array<ArrayBuffer>} k - 32-byte public key
+ * @returns true if signature matches block hash and public key, else false
+ */
+export function verify (s: Uint8Array<ArrayBuffer>, m: Uint8Array<ArrayBuffer>, k: Uint8Array<ArrayBuffer>): boolean
+export function verify (s: string | Uint8Array<ArrayBuffer>, m: string | Uint8Array<ArrayBuffer>, k: string | Uint8Array<ArrayBuffer>): boolean {
+ return nano25519.verify(s, m, k)
+}
//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { derive, deriveAsync, sign, signAsync, verify, verifyAsync } from 'nano-nacl'
+import * as Nano25519 from 'nano25519'
+import { derive, sign, verify } from 'nano25519/sync'
+import { deriveAsync, signAsync, verifyAsync } from 'nano25519/async'
import { NANO_ORG_VECTOR, PYTHON_ED25519_BLAKE2B_VECTORS } from './vectors.mjs'
/**
let result, test, passes = 0, failures = 0
-/**
- * @type {any} result
- */
+// Check combined imports
+result = await Nano25519.deriveAsync(NANO_ORG_VECTOR.privateKeyBytes)
+console.log(`RESULT`, result)
+test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase()
+check(`async derive from ${NANO_ORG_VECTOR.privateKey}`, test)
+passes += +test
+failures += +!test
+
+result = await Nano25519.signAsync(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes)
+test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase()
+check(`async sign message ${NANO_ORG_VECTOR.blockHash}`, test)
+passes += +test
+failures += +!test
+
+result = await Nano25519.verifyAsync(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes)
+test = result === true
+check(`async verify signature ${NANO_ORG_VECTOR.signature}`, test)
+passes += +test
+failures += +!test
+
+result = Nano25519.derive(NANO_ORG_VECTOR.privateKeyBytes)
+test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase()
+check(`derive from ${NANO_ORG_VECTOR.privateKey}`, test)
+passes += +test
+failures += +!test
+
+result = Nano25519.sign(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes)
+test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase()
+check(`sign message ${NANO_ORG_VECTOR.blockHash}`, test)
+passes += +test
+failures += +!test
+
+result = Nano25519.verify(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes)
+test = result === true
+check(`verify signature ${NANO_ORG_VECTOR.signature}`, test)
+passes += +test
+failures += +!test
+
+// Check async imports with string inputs
result = await deriveAsync(NANO_ORG_VECTOR.privateKey)
test = result.toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase()
-check(`async derive from ${NANO_ORG_VECTOR.privateKey}`, test)
+console.log(result)
+console.log()
+check(`async derive from private key string ${NANO_ORG_VECTOR.privateKey}`, test)
passes += +test
failures += +!test
result = await signAsync(NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.secretKey)
test = result.toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase()
-check(`async sign message ${NANO_ORG_VECTOR.blockHash}`, test)
+check(`async sign message string ${NANO_ORG_VECTOR.blockHash}`, test)
passes += +test
failures += +!test
result = await verifyAsync(NANO_ORG_VECTOR.signature, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey)
test = result === true
-check(`async verify signature ${NANO_ORG_VECTOR.signature}`, test)
+check(`async verify signature string ${NANO_ORG_VECTOR.signature}`, test)
+passes += +test
+failures += +!test
+
+// Check async imports with byte inputs
+result = await deriveAsync(NANO_ORG_VECTOR.privateKeyBytes)
+console.log(`RESULT`, result)
+test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase()
+check(`async derive from private key bytes ${NANO_ORG_VECTOR.privateKey}`, test)
+passes += +test
+failures += +!test
+
+result = await signAsync(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes)
+test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase()
+check(`async sign message bytes ${NANO_ORG_VECTOR.blockHash}`, test)
+passes += +test
+failures += +!test
+
+result = await verifyAsync(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes)
+test = result === true
+check(`async verify signature bytes ${NANO_ORG_VECTOR.signature}`, test)
+passes += +test
+failures += +!test
+
+// Check sync imports with string inputs
+result = derive(NANO_ORG_VECTOR.privateKey)
+test = result.toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase()
+check(`derive from private key string ${NANO_ORG_VECTOR.privateKey}`, test)
+passes += +test
+failures += +!test
+
+result = sign(NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.secretKey)
+test = result.toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase()
+check(`sign message string ${NANO_ORG_VECTOR.blockHash}`, test)
+passes += +test
+failures += +!test
+
+result = verify(NANO_ORG_VECTOR.signature, NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.publicKey)
+test = result === true
+check(`verify signature string ${NANO_ORG_VECTOR.signature}`, test)
passes += +test
failures += +!test
+// Check sync imports with byte inputs
result = derive(NANO_ORG_VECTOR.privateKeyBytes)
test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase()
-check(`derive from ${NANO_ORG_VECTOR.privateKey}`, test)
+check(`derive from private key string ${NANO_ORG_VECTOR.privateKey}`, test)
passes += +test
failures += +!test
result = sign(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.secretKeyBytes)
test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase()
-check(`sign message ${NANO_ORG_VECTOR.blockHash}`, test)
+check(`sign message string ${NANO_ORG_VECTOR.blockHash}`, test)
passes += +test
failures += +!test
result = verify(NANO_ORG_VECTOR.signatureBytes, NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.publicKeyBytes)
test = result === true
-check(`verify signature ${NANO_ORG_VECTOR.signature}`, test)
+check(`verify signature string ${NANO_ORG_VECTOR.signature}`, test)
passes += +test
failures += +!test
+// test both string inputs and byte inputs
for (const { privateKey, publicKey, message, signature } of PYTHON_ED25519_BLAKE2B_VECTORS) {
+ result = derive(privateKey)
+ test = result.toLowerCase() === publicKey
+ check(`derive from private key string ${privateKey}`, test)
+ passes += +test
+ failures += +!test
+
+ result = sign(message, privateKey + publicKey)
+ test = result.toLowerCase() === signature.slice(0, 128)
+ check(`sign message string ${message}`, test)
+ passes += +test
+ failures += +!test
+
+ result = verify(signature.slice(0, 128), message, publicKey)
+ test = result === true
+ check(`verify signature string ${signature.slice(0, 128)}`, test)
+ passes += +test
+ failures += +!test
+
const privateKeyBytes = new Uint8Array(privateKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
const publicKeyBytes = new Uint8Array(publicKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
const secretKeyBytes = new Uint8Array([...privateKeyBytes, ...publicKeyBytes])
result = derive(privateKeyBytes)
test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === publicKey
- check(`derive from ${privateKey}`, test)
+ check(`derive from private key bytes ${privateKey}`, test)
passes += +test
failures += +!test
result = sign(messageBytes, secretKeyBytes)
test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === signature.slice(0, 128)
- check(`sign message ${message}`, test)
+ check(`sign message bytes ${message}`, test)
passes += +test
failures += +!test
result = verify(signatureBytes, messageBytes, publicKeyBytes)
test = result === true
- check(`verify signature ${signature.slice(0, 128)}`, test)
+ check(`verify signature bytes ${signature.slice(0, 128)}`, test)
passes += +test
failures += +!test
}
"ESNext"
]
},
+ "include": [
+ "src/index.ts",
+ "src/async.ts",
+ "src/sync.ts"
+ ],
"exclude": [
"assembly",
"dist"