UNPKG

kruptein

Version:

crypto; from kruptein to hide or conceal

463 lines (341 loc) 10.4 kB
/*! * kruptein * Copyright(c) 2019 Jason Gerfen <jason.gerfen@gmail.com> * License: MIT */ "use strict"; class Kruptein { constructor(options) { options = options || {}; this.crypto = require("crypto"); this._algorithm = options.algorithm || "aes-256-gcm"; this._hashing = options.hashing || "sha384"; this._encodeas = options.encodeas || "base64"; this._use_asn1 = options.use_asn1 ?? true; this._aead_mode = this._algorithm.match(/ccm|gcm|ocb/) ? true : false; let defaults = this._matrix(this._algorithm); this._at_size = options._at_size || defaults._at_size; this._iv_size = options._iv_size || defaults._iv_size; this._key_size = options._key_size || defaults._key_size; this._use_scrypt = options.use_scrypt || false; this._use_argon2 = options.use_argon2 || false; if (this._use_argon2) { this._argon_parameters = { parallelism: 4, tagLength: this._key_size, memory: 65536, passes: 3, }; } if (this._use_asn1) { this.asn1 = require("@jas-/asn.1"); this.schema = this._schema(); } this._keyCache = new Map(); } set(secret, plaintext, aad, cb) { cb = cb || aad; let iv, ct, hmac, obj, key, material; if (this._validator()) return cb("Insecure cipher mode not supported!"); if (!this._complexity(secret)) return cb("The supplied secret failed to meet complexity requirements!"); if (this._key) { key = this._key; } else { this._derive_key(secret, (err, secret) => { if (err) return cb("Unable to derive key!"); key = secret; this._key = key; }); } iv = this._iv(this._iv_size); let pt = plaintext; try { plaintext = Buffer.from(JSON.stringify(pt)); } catch(err) { plaintext = Buffer.from(pt); } if (this._aead_mode && typeof aad === "function") { this._digest(this._iv(128), plaintext, this._hashing, this._encodeas, (err, res) => { if (err) return cb("Unable to generate AAD!"); aad = res; }); } this._encrypt(key.key, plaintext, this._algorithm, this._encodeas, iv, aad, (err, ciphertext) => { if (err) return cb("Unable to create ciphertext!"); ct = ciphertext; }); material = Buffer.from(ct.ct + key.salt + iv); this._digest(key.key, material, this._hashing, this._encodeas, (err, digest) => { if (err) return cb("Unable to create digest!"); hmac = digest; }); obj = { hmac: hmac, ct: ct.ct, iv: iv, salt: key.salt }; if (aad) obj.aad = aad; if (ct.at) obj.at = ct.at; return (this._use_asn1) ? cb(null, this.schema.encode(obj).toString(this._encodeas)) : cb(null, JSON.stringify(obj)); } get(secret, ciphertext, opts, cb) { cb = cb || opts; let ct, hmac, hmac_bytes, orig_hmac_bytes, pt, key, material, valid; if (this._validator()) return cb("Insecure cipher mode not supported!"); if (!secret) return cb("Must supply a secret!"); try { if (this._use_asn1) { ct = this.schema.decode(Buffer.from(ciphertext, this._encodeas)); ct.hmac = ct.hmac.toString(); if (ct.at) ct.ct = ct.ct.toString(); if (ct.aad) ct.aad = ct.aad.toString(); } else { ct = JSON.parse(ciphertext); } } catch (err) { return cb("Unable to parse ciphertext object!"); } if (this._key) { key = this._key; } else { this._derive_key(secret, ct.salt, (err, secret) => { if (err) return cb("Unable to derive key!"); key = secret; }); } material = Buffer.from(ct.ct + ct.salt + ct.iv); this._digest(key.key, material, this._hashing, this._encodeas, (err, res) => { if (err) cb("Unable to generate HMAC!"); hmac = res; }); hmac_bytes = Buffer.from(hmac); orig_hmac_bytes = Buffer.from(ct.hmac); if (hmac_bytes.byteLength !== orig_hmac_bytes.byteLength) { return cb("Encrypted session was tampered with!"); } else { if (typeof this.crypto.timingSafeEqual === "function") { try { valid = this.crypto.timingSafeEqual(hmac_bytes, orig_hmac_bytes); } catch(err) { return cb("Encrypted session was tampered with! (timing)"); } } } if (opts) { ct.aad = (opts.aad) ? opts.aad : (ct.aad) ? ct.aad : false; ct.at = (opts.at && !ct.at) ? opts.at : (ct.at) ? ct.at : false; } if (ct.at) ct.at = Buffer.from(ct.at, this._encodeas); this._decrypt(key.key, ct.ct, this._algorithm, this._encodeas, Buffer.from(ct.iv, this._encodeas), ct.at, ct.aad, (err, res) => { if (err) return cb("Unable to decrypt ciphertext!"); pt = res; }); return cb(null, pt); } _encrypt(key, pt, algo, encodeas, iv, aad, cb) { cb = cb || aad; let cipher, ct, at; cipher = this.crypto.createCipheriv(algo, key, iv, { authTagLength: this._at_size }); if (this._aead_mode && typeof aad !== "function") { try { cipher.setAAD(Buffer.from(aad, encodeas), { plaintextLength: Buffer.byteLength(pt) }); } catch (err) { return cb("Unable to set AAD!"); } } ct = cipher.update(Buffer.from(pt, encodeas), "utf8", encodeas); cipher.setAutoPadding(true); ct += cipher.final(encodeas); if (this._aead_mode) { try { at = cipher.getAuthTag(); } catch (err) { return cb("Unable to obtain authentication tag"); } } return cb(null, (at) ? { "ct": ct, "at": at } : { "ct": ct }); } _decrypt(key, ct, algo, encodeas, iv, at, aad, cb) { cb = cb || aad; let cipher, pt; cipher = this.crypto.createDecipheriv(algo, key, iv, { authTagLength: this._at_size }); if (this._aead_mode && at) { try { cipher.setAuthTag(Buffer.from(at, encodeas)); } catch (err) { return cb("Unable to set authentication tag"); } } if (this._aead_mode && typeof aad !== "function") { try { cipher.setAAD(Buffer.from(aad, encodeas), { plaintextLength: ct.length }); } catch (err) { return cb("Unable to set additional authentication data"); } } try { pt = cipher.update(ct.toString(), encodeas, "utf8"); pt += cipher.final("utf8"); } catch(err) { return cb("Unable to decrypt ciphertext!"); } return cb(null, pt); } _get_cached_key() { return this._key || false; } _cache_key(key) { this._key = key; } _derive_key(secret, salt, cb) { cb = cb || salt; let key, opts = {}; if (typeof secret === "object") { opts = secret.opts; secret = secret.secret; } salt = (typeof salt !== "function") ? Buffer.from(salt) : this.crypto.randomBytes(128); try { if (this._use_scrypt && typeof this.crypto.scryptSync === "function") { key = this.crypto.scryptSync(secret, salt, this._key_size, opts); } else if (this._use_argon2 && typeof this.crypto.argon2Sync === "function") { this._argon_parameters.message = Buffer.from(secret); this._argon_parameters.nonce = salt; key = this.crypto.argon2Sync('argon2id', this._argon_parameters); } else { key = this.crypto.pbkdf2Sync(secret, salt, 20000, this._key_size, this._hashing); } } catch (err) { return cb("Unable to derive key!"); } return cb(null, { key: key, salt: salt }); } _digest(key, obj, hashing, encodeas, cb) { let hmac; try { hmac = this.crypto.createHmac(hashing, key); hmac.setEncoding(encodeas); hmac.write(obj); hmac.end(); } catch (err) { return cb("Unable to generate digest!"); } return cb(null, hmac.read().toString(encodeas)); } _iv(iv_size) { return this.crypto.randomBytes(iv_size); } _matrix(algo) { let obj = { _at_size: 16, _iv_size: 16, _key_size: 32 }; if (algo.match(/ccm|ocb|gcm/i)) obj._iv_size = 12; if (algo.match(/aes/) && algo.match(/128/)) obj._key_size = 16; if (algo.match(/aes/) && algo.match(/192/)) obj._key_size = 24; if (algo.match(/aes/) && algo.match(/xts/)) obj._key_size = 32; if (algo.match(/aes/) && algo.match(/xts/) && algo.match(/256/)) obj._key_size = 64; return obj; } _validator() { return (this._algorithm.match(/ccm|ecb|ocb2|xts/)); } _schema() { let schema; if (!this._aead_mode) { schema = this.asn1.define('schema', function() { this.seq().obj( this.key("ct").octstr(), this.key("hmac").octstr(), this.key("iv").octstr(), this.key("salt").octstr() ); }); } else { schema = this.asn1.define('schema', function() { this.seq().obj( this.key("ct").octstr(), this.key("hmac").octstr(), this.key("iv").octstr(), this.key("salt").octstr(), this.key("at").octstr(), this.key("aad").octstr() ); }); } return schema; } _complexity(str) { const tests = { min_length: 8, min_upper: 2, min_lower: 2, min_numbers: 2, min_special: 2 } if (!str || str === "" || typeof str === 'undefined') return false; if (str.length < tests.min_length) return false; let min_upper_matches = str.match(/[A-Z]/g); if (min_upper_matches.length < tests.min_upper) return false; let min_lower_matches = str.match(/[a-z]/g); if (min_lower_matches.length < tests.min_lower) return false; let min_numbers_matches = str.match(/[0-9]/g); if (min_numbers_matches.length < tests.min_numbers) return false; let min_special_matches = str.match(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/g); if (min_special_matches.length < tests.min_special) return false; return true; } } let instance = null; module.exports = function(options) { if (!instance) { return new Kruptein(options || {}); } return instance; };