tsoa
This commit is contained in:
373
node_modules/@hapi/iron/lib/index.js
generated
vendored
Executable file
373
node_modules/@hapi/iron/lib/index.js
generated
vendored
Executable file
@@ -0,0 +1,373 @@
|
||||
'use strict';
|
||||
|
||||
const Crypto = require('crypto');
|
||||
|
||||
const B64 = require('@hapi/b64');
|
||||
const Boom = require('@hapi/boom');
|
||||
const Bourne = require('@hapi/bourne');
|
||||
const Cryptiles = require('@hapi/cryptiles');
|
||||
const Hoek = require('@hapi/hoek');
|
||||
|
||||
|
||||
const internals = {};
|
||||
|
||||
|
||||
exports.defaults = {
|
||||
encryption: {
|
||||
saltBits: 256,
|
||||
algorithm: 'aes-256-cbc',
|
||||
iterations: 1,
|
||||
minPasswordlength: 32
|
||||
},
|
||||
|
||||
integrity: {
|
||||
saltBits: 256,
|
||||
algorithm: 'sha256',
|
||||
iterations: 1,
|
||||
minPasswordlength: 32
|
||||
},
|
||||
|
||||
ttl: 0, // Milliseconds, 0 means forever
|
||||
timestampSkewSec: 60, // Seconds of permitted clock skew for incoming expirations
|
||||
localtimeOffsetMsec: 0 // Local clock time offset express in a number of milliseconds (positive or negative)
|
||||
};
|
||||
|
||||
|
||||
// Algorithm configuration
|
||||
|
||||
exports.algorithms = {
|
||||
'aes-128-ctr': { keyBits: 128, ivBits: 128 },
|
||||
'aes-256-cbc': { keyBits: 256, ivBits: 128 },
|
||||
'sha256': { keyBits: 256 }
|
||||
};
|
||||
|
||||
|
||||
// MAC normalization format version
|
||||
|
||||
exports.macFormatVersion = '2'; // Prevent comparison of mac values generated with different normalized string formats
|
||||
|
||||
exports.macPrefix = 'Fe26.' + exports.macFormatVersion;
|
||||
|
||||
|
||||
// Generate a unique encryption key
|
||||
|
||||
/*
|
||||
const options = {
|
||||
saltBits: 256, // Ignored if salt is set
|
||||
salt: '4d8nr9q384nr9q384nr93q8nruq9348run',
|
||||
algorithm: 'aes-128-ctr',
|
||||
iterations: 10000,
|
||||
iv: 'sdfsdfsdfsdfscdrgercgesrcgsercg', // Optional
|
||||
minPasswordlength: 32
|
||||
};
|
||||
*/
|
||||
|
||||
exports.generateKey = async function (password, options) {
|
||||
|
||||
if (!password) {
|
||||
throw new Boom.Boom('Empty password');
|
||||
}
|
||||
|
||||
if (!options ||
|
||||
typeof options !== 'object') {
|
||||
|
||||
throw new Boom.Boom('Bad options');
|
||||
}
|
||||
|
||||
const algorithm = exports.algorithms[options.algorithm];
|
||||
if (!algorithm) {
|
||||
throw new Boom.Boom('Unknown algorithm: ' + options.algorithm);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
|
||||
if (Buffer.isBuffer(password)) {
|
||||
if (password.length < algorithm.keyBits / 8) {
|
||||
throw new Boom.Boom('Key buffer (password) too small');
|
||||
}
|
||||
|
||||
result.key = password;
|
||||
result.salt = '';
|
||||
}
|
||||
else {
|
||||
if (password.length < options.minPasswordlength) {
|
||||
throw new Boom.Boom('Password string too short (min ' + options.minPasswordlength + ' characters required)');
|
||||
}
|
||||
|
||||
let salt = options.salt;
|
||||
if (!salt) {
|
||||
if (!options.saltBits) {
|
||||
throw new Boom.Boom('Missing salt and saltBits options');
|
||||
}
|
||||
|
||||
const randomSalt = Cryptiles.randomBits(options.saltBits);
|
||||
salt = randomSalt.toString('hex');
|
||||
}
|
||||
|
||||
const derivedKey = await internals.pbkdf2(password, salt, options.iterations, algorithm.keyBits / 8, 'sha1');
|
||||
|
||||
result.key = derivedKey;
|
||||
result.salt = salt;
|
||||
}
|
||||
|
||||
if (options.iv) {
|
||||
result.iv = options.iv;
|
||||
}
|
||||
else if (algorithm.ivBits) {
|
||||
result.iv = Cryptiles.randomBits(algorithm.ivBits);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
// Encrypt data
|
||||
// options: see exports.generateKey()
|
||||
|
||||
exports.encrypt = async function (password, options, data) {
|
||||
|
||||
const key = await exports.generateKey(password, options);
|
||||
const cipher = Crypto.createCipheriv(options.algorithm, key.key, key.iv);
|
||||
const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
|
||||
|
||||
return { encrypted, key };
|
||||
};
|
||||
|
||||
|
||||
// Decrypt data
|
||||
// options: see exports.generateKey()
|
||||
|
||||
exports.decrypt = async function (password, options, data) {
|
||||
|
||||
const key = await exports.generateKey(password, options);
|
||||
const decipher = Crypto.createDecipheriv(options.algorithm, key.key, key.iv);
|
||||
let dec = decipher.update(data, null, 'utf8');
|
||||
dec = dec + decipher.final('utf8');
|
||||
|
||||
return dec;
|
||||
};
|
||||
|
||||
|
||||
// HMAC using a password
|
||||
// options: see exports.generateKey()
|
||||
|
||||
exports.hmacWithPassword = async function (password, options, data) {
|
||||
|
||||
const key = await exports.generateKey(password, options);
|
||||
const hmac = Crypto.createHmac(options.algorithm, key.key).update(data);
|
||||
const digest = hmac.digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
|
||||
|
||||
return {
|
||||
digest,
|
||||
salt: key.salt
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// Normalizes a password parameter into a { id, encryption, integrity } object
|
||||
// password: string, buffer or object with { id, secret } or { id, encryption, integrity }
|
||||
|
||||
internals.normalizePassword = function (password) {
|
||||
|
||||
if (password &&
|
||||
typeof password === 'object' &&
|
||||
!Buffer.isBuffer(password)) {
|
||||
|
||||
return {
|
||||
id: password.id,
|
||||
encryption: password.secret ?? password.encryption,
|
||||
integrity: password.secret ?? password.integrity
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
encryption: password,
|
||||
integrity: password
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// Encrypt and HMAC an object
|
||||
// password: string, buffer or object with { id, secret } or { id, encryption, integrity }
|
||||
// options: see exports.defaults
|
||||
|
||||
exports.seal = async function (object, password, options) {
|
||||
|
||||
options = Object.assign({}, options); // Shallow cloned to prevent changes during async operations
|
||||
|
||||
const now = Date.now() + (options.localtimeOffsetMsec ?? 0); // Measure now before any other processing
|
||||
|
||||
// Serialize object
|
||||
|
||||
const objectString = internals.stringify(object);
|
||||
|
||||
// Obtain password
|
||||
|
||||
let passwordId = '';
|
||||
password = internals.normalizePassword(password);
|
||||
if (password.id) {
|
||||
if (!/^\w+$/.test(password.id)) {
|
||||
throw new Boom.Boom('Invalid password id');
|
||||
}
|
||||
|
||||
passwordId = password.id;
|
||||
}
|
||||
|
||||
// Encrypt object string
|
||||
|
||||
const { encrypted, key } = await exports.encrypt(password.encryption, options.encryption, objectString);
|
||||
|
||||
// Base64url the encrypted value
|
||||
|
||||
const encryptedB64 = B64.base64urlEncode(encrypted);
|
||||
const iv = B64.base64urlEncode(key.iv);
|
||||
const expiration = (options.ttl ? now + options.ttl : '');
|
||||
const macBaseString = exports.macPrefix + '*' + passwordId + '*' + key.salt + '*' + iv + '*' + encryptedB64 + '*' + expiration;
|
||||
|
||||
// Mac the combined values
|
||||
|
||||
const mac = await exports.hmacWithPassword(password.integrity, options.integrity, macBaseString);
|
||||
|
||||
// Put it all together
|
||||
|
||||
// prefix*[password-id]*encryption-salt*encryption-iv*encrypted*[expiration]*hmac-salt*hmac
|
||||
// Allowed URI query name/value characters: *-. \d \w
|
||||
|
||||
const sealed = macBaseString + '*' + mac.salt + '*' + mac.digest;
|
||||
return sealed;
|
||||
};
|
||||
|
||||
|
||||
// Decrypt and validate sealed string
|
||||
// password: string, buffer or object with { id: secret } or { id: { encryption, integrity } }
|
||||
// options: see exports.defaults
|
||||
|
||||
exports.unseal = async function (sealed, password, options) {
|
||||
|
||||
options = Object.assign({}, options); // Shallow cloned to prevent changes during async operations
|
||||
|
||||
const now = Date.now() + (options.localtimeOffsetMsec ?? 0); // Measure now before any other processing
|
||||
|
||||
// Break string into components
|
||||
|
||||
const parts = sealed.split('*');
|
||||
if (parts.length !== 8) {
|
||||
throw new Boom.Boom('Incorrect number of sealed components');
|
||||
}
|
||||
|
||||
const macPrefix = parts[0];
|
||||
const passwordId = parts[1];
|
||||
const encryptionSalt = parts[2];
|
||||
const encryptionIv = parts[3];
|
||||
const encryptedB64 = parts[4];
|
||||
const expiration = parts[5];
|
||||
const hmacSalt = parts[6];
|
||||
const hmac = parts[7];
|
||||
const macBaseString = macPrefix + '*' + passwordId + '*' + encryptionSalt + '*' + encryptionIv + '*' + encryptedB64 + '*' + expiration;
|
||||
|
||||
// Check prefix
|
||||
|
||||
if (macPrefix !== exports.macPrefix) {
|
||||
throw new Boom.Boom('Wrong mac prefix');
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
|
||||
if (expiration) {
|
||||
if (!expiration.match(/^\d+$/)) {
|
||||
throw new Boom.Boom('Invalid expiration');
|
||||
}
|
||||
|
||||
const exp = parseInt(expiration, 10);
|
||||
if (exp <= (now - (options.timestampSkewSec * 1000))) {
|
||||
throw new Boom.Boom('Expired seal');
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain password
|
||||
|
||||
if (!password) {
|
||||
throw new Boom.Boom('Empty password');
|
||||
}
|
||||
|
||||
if (typeof password === 'object' &&
|
||||
!Buffer.isBuffer(password)) {
|
||||
|
||||
password = password[passwordId || 'default'];
|
||||
if (!password) {
|
||||
throw new Boom.Boom('Cannot find password: ' + passwordId);
|
||||
}
|
||||
}
|
||||
|
||||
password = internals.normalizePassword(password);
|
||||
|
||||
// Check hmac
|
||||
|
||||
const macOptions = Hoek.clone(options.integrity);
|
||||
macOptions.salt = hmacSalt;
|
||||
const mac = await exports.hmacWithPassword(password.integrity, macOptions, macBaseString);
|
||||
|
||||
if (!Cryptiles.fixedTimeComparison(mac.digest, hmac)) {
|
||||
throw new Boom.Boom('Bad hmac value');
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
|
||||
try {
|
||||
var encrypted = B64.base64urlDecode(encryptedB64, 'buffer');
|
||||
}
|
||||
catch (err) {
|
||||
throw Boom.boomify(err);
|
||||
}
|
||||
|
||||
const decryptOptions = Hoek.clone(options.encryption);
|
||||
decryptOptions.salt = encryptionSalt;
|
||||
|
||||
try {
|
||||
decryptOptions.iv = B64.base64urlDecode(encryptionIv, 'buffer');
|
||||
}
|
||||
catch (err) {
|
||||
throw Boom.boomify(err);
|
||||
}
|
||||
|
||||
const decrypted = await exports.decrypt(password.encryption, decryptOptions, encrypted);
|
||||
|
||||
// Parse JSON
|
||||
|
||||
try {
|
||||
return Bourne.parse(decrypted);
|
||||
}
|
||||
catch (err) {
|
||||
throw new Boom.Boom('Failed parsing sealed object JSON: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
internals.stringify = function (object) {
|
||||
|
||||
try {
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
catch (err) {
|
||||
throw new Boom.Boom('Failed to stringify object: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
internals.pbkdf2 = function (...args) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const next = (err, result) => {
|
||||
|
||||
if (err) {
|
||||
return reject(Boom.boomify(err));
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
args.push(next);
|
||||
Crypto.pbkdf2(...args);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user