UNPKG

branca

Version:

Authenticated and encrypted API tokens using modern crypto

109 lines (88 loc) 3.26 kB
"use strict"; const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; const NONCE_BYTES = 24; const HEADER_BYTES = 29; const HEADER_FORMAT = ">B(version)L(timestamp)BBBBBBBBBBBBBBBBBBBBBBBB(nonce)"; const base62 = require("base-x")(BASE62) const bufferpack = require("bufferpack"); const sodium = require("libsodium-wrappers"); let Branca = function (key) { this.version = 0xBA; /* https://github.com/Microsoft/TypeScript/issues/14107 */ if (typeof key === "string") { key = Buffer.from(key, "hex"); } this.key = key; /* Used only for unit testing. */ this._nonce = null; /* Secret key must be 32 bytes */ if (!this.key || this.key.length !== sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES) { throw new Error(`Invalid key length. Expected ${sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES}, got ${this.key.length}`); } }; Branca.prototype.encode = function (message, timestamp) { let nonce; if (this._nonce) { /* Hey you! Yes, you. Do not set nonce yourself in production. */ /* You will shoot yourself in the foot.*/ nonce = this._nonce; } else { nonce = sodium.randombytes_buf(NONCE_BYTES); } /* Create timestamp if nothing was passed. */ if (undefined === timestamp) { timestamp = Math.floor(new Date() / 1000); } /* Header is the AD part of AEAD. It is authenticated but not encrypted. */ let header = bufferpack.pack(HEADER_FORMAT, [this.version, timestamp, ...nonce]); let ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( message, header, nonce, nonce, this.key ); let binary = Buffer.concat([header, Buffer.from(ciphertext)]); return base62.encode(binary); }; Branca.prototype.decode = function (token, ttl) { let binary = base62.decode(token); let header = binary.slice(0, HEADER_BYTES); let ciphertext = binary.slice(HEADER_BYTES, binary.length); let unpacked = bufferpack.unpack(HEADER_FORMAT, header); let version = unpacked.shift(); let timestamp = unpacked.shift(); let nonce = Buffer.from(unpacked); /* Implementation should accept only one current version. */ if (version !== this.version) { throw new Error("Invalid token version."); } /* Header was extracted from the binary token. */ let payload = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( nonce, ciphertext, header, nonce, this.key ); /* Check for expiration only when requested by passing in a TTL. */ if (undefined !== ttl) { let future = timestamp + ttl; let unixtime = Math.round(Date.now() / 1000); if (future < unixtime) { throw new Error("Token is expired."); }; }; return Buffer.from(payload); }; Branca.prototype.timestamp = function (token) { let binary = base62.decode(token); let header = binary.slice(0, HEADER_BYTES); let unpacked = bufferpack.unpack(HEADER_FORMAT, header); let version = unpacked.shift(); let timestamp = unpacked.shift(); return timestamp; }; module.exports = function(key) { return new Branca(key); }