TweetNaCl signing and box encryption: working versions (tested 2026-10-16)
Sign and verify messages with Ed25519 detached signatures and encrypt with authenticated public-key box using tweetnacl, fully offline.
passed7 passed · 0 failed2026-10-16 02:18 UTC- Status
- passed
- Last passed
- 2026-10-16 02:18 UTC
- Tests
- 7 / 7
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions3
tweetnacl@1.0.3@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38839686104
The code
Exactly the files that passed. Your agent can pull them with get_recipe("tweetnacl-sign-box") (install), or see them on GitHub.
package.json
{
"name": "tweetnacl-sign-box",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"tweetnacl": "1.0.3"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/sign.ts
import nacl from "tweetnacl";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export type SigningKeyPair = {
publicKey: Uint8Array;
secretKey: Uint8Array;
};
export function createSigningKeyPair(): SigningKeyPair {
return nacl.sign.keyPair();
}
export function sign(message: string, secretKey: Uint8Array): Uint8Array {
return nacl.sign.detached(encoder.encode(message), secretKey);
}
export function verify(
message: string,
signature: Uint8Array,
publicKey: Uint8Array,
): boolean {
return nacl.sign.detached.verify(encoder.encode(message), signature, publicKey);
}
export function signAttached(message: string, secretKey: Uint8Array): Uint8Array {
return nacl.sign(encoder.encode(message), secretKey);
}
export function openSigned(
signed: Uint8Array,
publicKey: Uint8Array,
): string | null {
const message = nacl.sign.open(signed, publicKey);
return message ? decoder.decode(message) : null;
}
src/box.ts
import nacl from "tweetnacl";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export type BoxKeyPair = {
publicKey: Uint8Array;
secretKey: Uint8Array;
};
export function createBoxKeyPair(): BoxKeyPair {
return nacl.box.keyPair();
}
export function randomNonce(): Uint8Array {
return nacl.randomBytes(nacl.box.nonceLength);
}
export function encrypt(
message: string,
nonce: Uint8Array,
theirPublicKey: Uint8Array,
mySecretKey: Uint8Array,
): Uint8Array {
return nacl.box(encoder.encode(message), nonce, theirPublicKey, mySecretKey);
}
export function decrypt(
ciphertext: Uint8Array,
nonce: Uint8Array,
theirPublicKey: Uint8Array,
mySecretKey: Uint8Array,
): string | null {
const message = nacl.box.open(
ciphertext,
nonce,
theirPublicKey,
mySecretKey,
);
return message ? decoder.decode(message) : null;
}
test/sign.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import nacl from "tweetnacl";
import {
createSigningKeyPair,
sign,
signAttached,
verify,
openSigned,
} from "../src/sign.ts";
test("detached signature verifies with the matching key", () => {
const keys = createSigningKeyPair();
const signature = sign("hello world", keys.secretKey);
assert.equal(signature.length, nacl.sign.signatureLength);
assert.equal(verify("hello world", signature, keys.publicKey), true);
});
test("tampered message fails verification", () => {
const keys = createSigningKeyPair();
const signature = sign("hello", keys.secretKey);
assert.equal(verify("hello!", signature, keys.publicKey), false);
});
test("signature from another key fails verification", () => {
const alice = createSigningKeyPair();
const bob = createSigningKeyPair();
const signature = sign("hello", alice.secretKey);
assert.equal(verify("hello", signature, bob.publicKey), false);
});
test("attached signature round-trips", () => {
const keys = createSigningKeyPair();
const signed = signAttached("hi there", keys.secretKey);
assert.equal(openSigned(signed, keys.publicKey), "hi there");
});
test/box.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import {
createBoxKeyPair,
decrypt,
encrypt,
randomNonce,
} from "../src/box.ts";
test("encrypts and decrypts between two key pairs", () => {
const alice = createBoxKeyPair();
const bob = createBoxKeyPair();
const nonce = randomNonce();
const ciphertext = encrypt("secret", nonce, bob.publicKey, alice.secretKey);
assert.notEqual(new TextDecoder().decode(ciphertext), "secret");
assert.equal(
decrypt(ciphertext, nonce, alice.publicKey, bob.secretKey),
"secret",
);
});
test("decryption fails with the wrong key", () => {
const alice = createBoxKeyPair();
const bob = createBoxKeyPair();
const eve = createBoxKeyPair();
const nonce = randomNonce();
const ciphertext = encrypt("secret", nonce, bob.publicKey, alice.secretKey);
assert.equal(
decrypt(ciphertext, nonce, eve.publicKey, eve.secretKey),
null,
);
});
test("rejects modified ciphertext", () => {
const alice = createBoxKeyPair();
const bob = createBoxKeyPair();
const nonce = randomNonce();
const ciphertext = encrypt("secret", nonce, bob.publicKey, alice.secretKey);
const tampered = Uint8Array.from(ciphertext);
tampered[0] ^= 0x01;
assert.equal(
decrypt(tampered, nonce, alice.publicKey, bob.secretKey),
null,
);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.