ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.