Back to packages list

Vals using tweetnacl

Description from the NPM package:
Port of TweetNaCl cryptographic library to JavaScript

good for session cookies or whatevs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { concat } from "https://deno.land/std@0.220.1/bytes/concat.ts";
import { decodeBase64, encodeBase64 } from "https://deno.land/std@0.220.1/encoding/base64.ts";
import nacl from "npm:tweetnacl@1.0.3";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
// $SECRET should be high quality random base64
const baseKey = decodeBase64(Deno.env.get("SECRET"));
function deriveKey(usage: string) {
const tag = encoder.encode(usage);
const hash = nacl.hash(concat([tag, baseKey]));
return hash;
}
export function encrypt(usage: string, data: Uint8Array): Uint8Array {
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
const key = deriveKey(usage).slice(0, nacl.secretbox.keyLength);
return concat([nonce, nacl.secretbox(data, nonce, key)]);
}
export function decrypt(usage: string, encrypted: Uint8Array): Uint8Array {
const box = encrypted.slice(nacl.secretbox.nonceLength);
const nonce = encrypted.slice(0, nacl.secretbox.nonceLength);
const key = deriveKey(usage).slice(0, nacl.secretbox.keyLength);
const data = nacl.secretbox.open(box, nonce, key);
if (!data) throw new Error("failed to decrypt");
return data;
}
export function encryptString(usage: string, data: string) {
return encodeBase64(encrypt(usage, encoder.encode(data)));
}
export function decryptString(usage: string, encrypted: string) {
return decoder.decode(decrypt(usage, decodeBase64(encrypted)));
}

tweetnacl example

An example of using tweetnacl from Val Town - this uses the keyPair method and then encodes the keypair using hex encoding.

1
2
3
4
5
6
7
8
9
10
11
export const tweetnacl = (async () => {
const { default: nacl } = await import("npm:tweetnacl");
const pair = nacl.box.keyPair();
return JSON.parse(JSON.stringify(pair, (k, r) => {
if (r instanceof Uint8Array) {
return Array.prototype.map.call(r, (b) => b.toString(16).padStart(2, "0"))
.join("");
}
return r;
}));
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Buffer } from "node:buffer";
export let naclValidateRequest = async (req: express.Request, publicKey) => {
const { default: nacl } = await import("npm:tweetnacl@1.0.3");
const signature = req.get("X-Signature-Ed25519");
const timestamp = req.get("X-Signature-Timestamp");
const body = JSON.stringify(req.body); // rawBody is expected to be a string, not raw bytes
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(signature, "hex"),
Buffer.from(publicKey, "hex"),
);
return isVerified;
};
1
2
3
4
5
6
7
8
9
10
11
import { Buffer } from "node:buffer";
export let naclValidate = async (publicKey, signature, timestamp, body) => {
const { default: nacl } = await import("npm:tweetnacl@1.0.3");
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(signature, "hex"),
Buffer.from(publicKey, "hex"),
);
return isVerified;
};

see @easrng.valSign for documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Buffer } from "node:buffer";
export async function valSignVerify(encoded: string) {
const matches = (encoded + "").match(/^@([^\.]+)\.(.+)$/);
if (!matches)
throw new Error("invalid signed data");
const [handle, encodedSignedMessage] = matches.slice(1);
const publicKey = Buffer.from(
await __utils__.api(handle + ".vsPublicKey"),
"base64",
);
const signedMessage = Buffer.from(encodedSignedMessage, "base64");
const { default: nacl } = await import("npm:tweetnacl@1.0.3");
const message = nacl.sign.open(signedMessage, publicKey);
if (!message)
throw new Error("invalid signature");
const { data, user, expr } = JSON.parse(new TextDecoder().decode(message));
if (user !== handle)
throw new Error("mismatched handle");
if (expr !== null && Date.now() > expr) {
throw new Error("signature expired");
}
return {
data,
handle,
expiresAt: expr === null ? null : new Date(expr),
};
}

secure signatures with vals

setup

you'll need to make 2 new vals:

  1. generate a keypair (keep this val private)
    Create vallet vsExportedKeys = @easrng.generateKeys();
  2. publish your public key (make this val public)
    Create valconst vsPublicKey = () => @me.exportedKeys.publicKey;

usage

sign

call @easrng.valSign to get a signature.

Create valconst signature = await @easrng.valSign({ keys: @me.vsExportedKeys, data: {hello: "world"}})

the result will look something like this:

@easrng.htVgaVWWtvnz5AK0DnDaNON5gar5qJeaorfsTCiIr7ua_-D4HPmFrIrPMfwmCaMvI0CxKlYCUe9XTGm7r5s5C3siZGF0YSI6eyJoZWxsbyI6IndvcmxkIn0sInVzZXIiOiJlYXNybmciLCJleHByIjpudWxsfQ

you can also set an expiration date:

Create valconst signature = await @easrng.valSign({ keys: @me.vsExportedKeys, data: "this expires in 1 second", expireIn: 1000 })

verify

call @easrng.valSignVerify to verify a signature

Create valconst { data, handle, expiresAt } = await @easrng.valSignVerify(signature)

with the example signature from earlier, data would be {hello: "world"}, handle would be easrng, and expiresAt would be null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { whoami } from "https://esm.town/v/easrng/whoami";
import { Buffer } from "node:buffer";
export async function valSign({ keys, data, expireIn }: {
keys: {
publicKey: string;
secretKey: string;
};
data: any;
expireIn?: number;
}) {
const topUser = whoami().at(-1).slice(1).split(".")[0];
const verifyPublicKey = await __utils__.api(topUser + ".vsPublicKey");
if (verifyPublicKey !== keys.publicKey)
throw new Error("keypair doesn't match @" + topUser + ".vsPublicKey()");
const { default: nacl } = await import("npm:tweetnacl@1.0.3");
const keyPair = {
publicKey: Buffer.from(keys.publicKey, "base64"),
secretKey: Buffer.from(keys.secretKey, "base64"),
};
const message = new TextEncoder().encode(JSON.stringify({
data,
user: topUser,
expr: expireIn ? Date.now() + expireIn : null,
}));
return "@" + topUser + "."
+ Buffer.from(nacl.sign(message, keyPair.secretKey)).toString("base64url");
}
1
2
3
4
5
6
7
8
9
10
import { Buffer } from "node:buffer";
export async function generateKeys() {
const { default: nacl } = await import("npm:tweetnacl@1.0.3");
const keyPair = nacl.sign.keyPair();
return {
publicKey: Buffer.from(keyPair.publicKey).toString("base64"),
secretKey: Buffer.from(keyPair.secretKey).toString("base64"),
};
}
1
Next