k9crypt
Version:
A special encryption algorithm created for K9Crypt.
177 lines (148 loc) • 7.75 kB
JavaScript
const crypto = require('crypto');
const { compress, decompress } = require('./utils/compression');
const { derivePayloadKey, deriveKey, hkdfExpandKey } = require('./utils/keyDerivation');
const { encrypt, decrypt, encryptAEAD, decryptAEAD } = require('./utils/encryption');
const { verifyHash, macHash, verifyMacHash } = require('./utils/hashing');
const { encryptFile, decryptFile } = require('./utils/stream');
const { encryptMany, decryptMany, encryptManyParallel, decryptManyParallel } = require('./utils/batch');
const { createTimeHeader, createPayloadBodyV5, buildPayload, decodePayload, parsePayload, validateFreshness, validateLegacyPolicy } = require('./utils/payload');
const { PAYLOAD_VERSION_V3, PAYLOAD_VERSION_V4, SALT_SIZE, IV_SIZE_V5, ARGON2_SALT_SIZE, MAX_PLAINTEXT_SIZE, MASTER_KEY_SALT } = require('./constants');
const norm = (v) => (v == null) ? {} : (typeof v !== 'object' || Array.isArray(v)) ? (() => { throw new TypeError('options must be an object'); })() : v;
const normLvl = (v) => { if (!Number.isInteger(v) || v < 0 || v > 9) throw new RangeError('compressionLevel must be 0-9'); return v; };
const normProg = (v) => v == null ? null : typeof v !== 'function' ? (() => { throw new TypeError('onProgress must be a function'); })() : v;
const resolveHashAuthKey = (payload, secretKey) => payload.version === PAYLOAD_VERSION_V3 ? secretKey : null;
const hasKeyedMacIntegrity = (p) => p.version >= PAYLOAD_VERSION_V4;
const isV5 = (p) => p.version >= 5;
const out = (payload, data) => payload.timeMetadata?.isBinary ? Buffer.from(data) : data.toString('utf8');
const restore = async (payload, decrypted) => payload.timeMetadata?.isCompressed === false ? decrypted : decompress(decrypted);
class K9crypt {
constructor(secretKey, opt = {}) {
opt = norm(opt);
if (secretKey == null) { this.secretKey = crypto.randomBytes(50); this._auto = true; }
else {
if (typeof secretKey !== 'string' && !Buffer.isBuffer(secretKey)) throw new TypeError('secretKey must be a string or Buffer');
if (!secretKey.length) throw new TypeError('secretKey must not be empty');
this._auto = false;
this.secretKey = Buffer.isBuffer(secretKey) ? Buffer.from(secretKey) : secretKey;
}
this.clvl = normLvl(opt.compressionLevel ?? 0);
this._mk = null;
}
getGenerated() { return this._auto ? Buffer.from(this.secretKey) : null; }
async _ensureMasterKey() { if (!this._mk) this._mk = await deriveKey(this.secretKey, MASTER_KEY_SALT); }
async encrypt(plaintext, opt = {}) {
try {
opt = norm(opt);
const buf = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, 'utf8');
if (buf.length > MAX_PLAINTEXT_SIZE) throw new Error('Payload too large');
const lvl = normLvl(opt.compressionLevel ?? this.clvl);
const comp = lvl > 0;
const tp = createTimeHeader({ ...opt, isBinary: Buffer.isBuffer(plaintext), isCompressed: comp });
const input = comp ? await compress(buf, lvl, !tp.timeMetadata.isBinary) : buf;
await this._ensureMasterKey();
const salt = crypto.randomBytes(SALT_SIZE);
const iv = crypto.randomBytes(IV_SIZE_V5);
let mk = null;
try {
mk = hkdfExpandKey(this._mk, salt, tp.timeMetadata);
const { encrypted: ct, tag } = encryptAEAD(input, mk, iv, tp.timeMetadata.cipherId, tp.header);
const body = createPayloadBodyV5({ header: tp.header, salt, iv, ciphertext: ct, tag });
const iSalt = crypto.randomBytes(ARGON2_SALT_SIZE);
return buildPayload(body, iSalt, macHash(body, iSalt, mk));
} finally { if (mk) mk.fill(0); }
} catch (e) { throw new Error('Encryption failed'); }
}
async decrypt(ct, opt = {}) {
try {
opt = norm(opt);
const payload = parsePayload(decodePayload(ct));
return isV5(payload) ? this._decV5(payload, opt) : this._decLegacy(payload, opt);
} catch (e) { throw new Error('Decryption failed'); }
}
async _decV5(p, opt) {
validateFreshness(p, opt);
await this._ensureMasterKey();
let mk = null;
try {
mk = hkdfExpandKey(this._mk, p.salt, p.timeMetadata);
if (!verifyMacHash(p.body, p.dataHash, p.integritySalt, mk)) throw new Error('Data integrity check failed');
const dec = decryptAEAD(p.ciphertext, mk, p.iv, p.tag, p.timeMetadata.cipherId, p.header);
const plain = await restore(p, dec);
if (plain.length > MAX_PLAINTEXT_SIZE) throw new Error('Payload too large');
return out(p, plain);
} finally { if (mk) mk.fill(0); }
}
async _decLegacy(p, opt) {
validateLegacyPolicy(p, opt);
const useKeyed = hasKeyedMacIntegrity(p);
if (!useKeyed && !(await verifyHash(p.body, p.dataHash, p.argon2Salt, resolveHashAuthKey(p, this.secretKey)))) {
throw new Error('Data integrity check failed');
}
validateFreshness(p, opt);
let key = null;
try {
key = await derivePayloadKey(this.secretKey, p.salt, p.timeMetadata);
if (useKeyed && !verifyMacHash(p.body, p.dataHash, p.argon2Salt, key)) throw new Error('Data integrity check failed');
const dec = decrypt(p.encrypted, key, p.iv1, p.iv2, p.iv3, p.iv4, p.iv5, p.tag1, p.header);
const plain = await restore(p, dec);
if (plain.length > MAX_PLAINTEXT_SIZE) throw new Error('Payload too large');
return out(p, plain);
} finally { if (key) key.fill(0); }
}
async encryptFile(plaintext, opt = {}) {
try {
opt = norm(opt);
await this._ensureMasterKey();
return encryptFile(plaintext, this.secretKey, this._mk, {
compressionLevel: normLvl(opt.compressionLevel ?? this.clvl),
onProgress: normProg(opt.onProgress),
timeStepSeconds: opt.timeStepSeconds, stepSeconds: opt.stepSeconds,
issuedAt: opt.issuedAt, issuedAtUnix: opt.issuedAtUnix
});
} catch (e) { throw new Error('File encryption failed'); }
}
async decryptFile(ct, opt = {}) {
try {
opt = norm(opt);
await this._ensureMasterKey();
return decryptFile(ct, this.secretKey, this._mk, {
onProgress: normProg(opt.onProgress),
allowLegacyPayloads: opt.allowLegacyPayloads,
maxAgeSeconds: opt.maxAgeSeconds, allowedClockSkewSeconds: opt.allowedClockSkewSeconds,
nowUnixSeconds: opt.nowUnixSeconds
});
} catch (e) { throw new Error('File decryption failed'); }
}
async encryptMany(arr, opt = {}) {
try {
opt = norm(opt);
await this._ensureMasterKey();
const opts = {
compressionLevel: normLvl(opt.compressionLevel ?? this.clvl),
onProgress: normProg(opt.onProgress),
timeStepSeconds: opt.timeStepSeconds, stepSeconds: opt.stepSeconds,
issuedAt: opt.issuedAt, issuedAtUnix: opt.issuedAtUnix
};
return opt.parallel
? encryptManyParallel(arr, this.secretKey, this._mk, { ...opts, batchSize: opt.batchSize })
: encryptMany(arr, this.secretKey, this._mk, opts);
} catch (e) { throw new Error('Multiple encryption failed'); }
}
async decryptMany(arr, opt = {}) {
try {
opt = norm(opt);
await this._ensureMasterKey();
const opts = {
skipInvalid: opt.skipInvalid || false,
onProgress: normProg(opt.onProgress),
allowLegacyPayloads: opt.allowLegacyPayloads,
maxAgeSeconds: opt.maxAgeSeconds, allowedClockSkewSeconds: opt.allowedClockSkewSeconds,
nowUnixSeconds: opt.nowUnixSeconds
};
return opt.parallel
? decryptManyParallel(arr, this.secretKey, this._mk, { ...opts, batchSize: opt.batchSize })
: decryptMany(arr, this.secretKey, this._mk, opts);
} catch (e) { throw new Error('Multiple decryption failed'); }
}
}
module.exports = K9crypt;