UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions, classes and types for everyday development needs.

65 lines (64 loc) 2.77 kB
import { isNonEmptyString } from '../guards/primitives.js'; import { isBase64 } from '../guards/specials.js'; import { _constantTimeEquals } from './helpers.js'; import { base64ToBytes, bytesToBase64, bytesToUtf8, concatBytes, hmacSha256, intTo4BytesBE, sha256Bytes, utf8ToBytes, } from './utils.js'; export class Cipher { #secretBytes; #encKey; #macKey; constructor(secret) { if (!isNonEmptyString(secret)) { throw new Error('Secret must be non-empty string!'); } this.#secretBytes = utf8ToBytes(secret); this.#encKey = hmacSha256(this.#secretBytes, utf8ToBytes('enc')); this.#macKey = hmacSha256(this.#secretBytes, utf8ToBytes('mac')); } #genKeystream(target, iv) { const blocks = Math.ceil(target.length / 32); const keystreamParts = []; for (let counter = 0; counter < blocks; counter++) { keystreamParts.push(hmacSha256(this.#encKey, concatBytes(iv, intTo4BytesBE(counter)))); } return concatBytes(...keystreamParts).subarray(0, target.length); } encrypt(text) { const plain = utf8ToBytes(text); const seed = utf8ToBytes(`${Date.now()}-${Math.random()}`); const ivFull = sha256Bytes(seed); const iv = ivFull.subarray(0, 16); const keystream = this.#genKeystream(plain, iv); const ct = plain.map((byte, i) => byte ^ keystream[i]); const tag = hmacSha256(this.#macKey, concatBytes(iv, ct)); return bytesToBase64(concatBytes(iv, ct, tag)); } isValid(token) { if (!isBase64(token)) return false; const blob = base64ToBytes(token); if (blob.length < 48) return false; const iv = blob.subarray(0, 16); const tag = blob.subarray(blob.length - 32); const ct = blob.subarray(16, blob.length - 32); const expectedTag = hmacSha256(this.#macKey, concatBytes(iv, ct)); return _constantTimeEquals(expectedTag, tag); } decrypt(token) { if (!isBase64(token)) throw new Error('Token must be a base64 string!'); const blob = base64ToBytes(token); if (blob.length < 48) throw new Error('Malformed or tampered token!'); const iv = blob.subarray(0, 16); const tag = blob.subarray(blob.length - 32); const ct = blob.subarray(16, blob.length - 32); const expectedTag = hmacSha256(this.#macKey, concatBytes(iv, ct)); if (!_constantTimeEquals(expectedTag, tag)) { throw new Error('Key in the token is tampered or invalid!'); } const keystream = this.#genKeystream(ct, iv); const pt = ct.map((byte, i) => byte ^ keystream[i]); return bytesToUtf8(pt); } }