paseto-ts
Version:
PASETO v4 (encrypt, decrypt, sign & verify) in TypeScript
69 lines (68 loc) • 4.23 kB
JavaScript
import { MAX_DEPTH_DEFAULT, MAX_KEYS_DEFAULT, TOKEN_MAGIC_BYTES, TOKEN_MAGIC_STRINGS } from "../lib/magic.js";
import { concat, payloadToUint8Array } from "../lib/uint8array.js";
import { deriveEncryptionAndAuthKeys, parseAssertion, parseFooter, parsePayload } from "../lib/parse.js";
import { PAE } from "../lib/pae.js";
import { base64UrlEncode } from "../lib/base64url.js";
import { hash } from "@stablelib/blake2b";
import { parseKeyData } from "../lib/parse.js";
import { streamXOR } from "@stablelib/xchacha20";
/**
* Encrypts a payload using a local key and returns a PASETO v4.local token
* The key must have the version and purpose of `k4.local`.
* @param {string | Uint8Array} key 32 bytes of data from a random source. Must be prepended with `k4.local.` before use.
* @param {Payload | string | Uint8array} payload Payload to encrypt
* @param {object} options Options
* @param {Footer | string | Uint8Array} options.footer Optional footer
* @param {Assertion | string | Uint8Array} options.assertion Optional assertion
* @param {number} options.maxDepth Maximum depth of nested objects in the payload and footer; defaults to 32
* @param {number} options.maxKeys Maximum number of keys in an object in the payload and footer; defaults to 128
* @param {(array: Uint8Array): Uint8Array} options.getRandomValues Optional crypto.getRandomValues implementation (for Node < 19)
* @returns {string} PASETO v4.local token
* @see https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#encrypt
*/
export function encrypt(key, payload, { footer = new Uint8Array(0), assertion = new Uint8Array(0), addIat = true, addExp = true, maxDepth = MAX_DEPTH_DEFAULT, maxKeys = MAX_KEYS_DEFAULT, validatePayload = true, getRandomValues = undefined, } = {
footer: new Uint8Array(0),
assertion: new Uint8Array(0),
addIat: true,
addExp: true,
maxDepth: MAX_DEPTH_DEFAULT,
maxKeys: MAX_KEYS_DEFAULT,
validatePayload: true,
getRandomValues: undefined,
}) {
// Assert that key is intended for use with v4.local tokens and has a length of 256 bits (32 bytes)
key = parseKeyData('local', key);
getRandomValues = getRandomValues ?? (typeof globalThis !== 'undefined' && globalThis.crypto?.getRandomValues ? globalThis.crypto.getRandomValues.bind(globalThis.crypto) : undefined);
if (!getRandomValues) {
throw new Error('No compatible getRandomValues implementation detected in the global scope. Please pass a getRandomValues implementation to the options object (signature: getRandomValues<Uint8Array>(array: Uint8Array): Uint8Array)');
}
const payloadUint8 = payloadToUint8Array(parsePayload(payload, {
addExp: !!addExp,
addIat: !!addIat,
maxDepth,
maxKeys,
validate: !!validatePayload,
}));
const footerUint8 = parseFooter(footer, {
maxDepth,
maxKeys,
validate: !!validatePayload,
});
// Assert assertion is a string or Uint8Array
assertion = parseAssertion(assertion);
// Generate 32 random bytes from the OS's CSPRNG
const nonce = getRandomValues(new Uint8Array(32));
// Derive encryption and authentication keys from the key and nonce
const { encryptionKey, counterNonce, authKey } = deriveEncryptionAndAuthKeys(key, nonce);
// Encrypt the message using XChaCha20
const ciphertext = streamXOR(encryptionKey, counterNonce, payloadUint8, new Uint8Array(payloadUint8.length));
// Pack header, nonce, ciphertext, footer, and assertion together using PAE
const preAuth = PAE(TOKEN_MAGIC_BYTES.v4.local, nonce, ciphertext, footerUint8, assertion);
// Calculate BLAKE2b-MAC of the output of preAuth using Ak as the authentication key
const tag = hash(preAuth, 32, { key: authKey });
// If footer is empty, return header || base64url(nonce || ciphertext || t)
// Otherwise, return header || base64url(nonce || ciphertext || t) || . || base64url(footer)
return footer.length === 0
? `${TOKEN_MAGIC_STRINGS.v4.local}${base64UrlEncode(concat(nonce, ciphertext, tag))}`
: `${TOKEN_MAGIC_STRINGS.v4.local}${base64UrlEncode(concat(nonce, ciphertext, tag))}.${base64UrlEncode(footerUint8)}`;
}