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
JavaScript
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);
}
}