UNPKG

node-laravel-encryptor

Version:
561 lines (493 loc) 14.6 kB
import {Serializer} from "./lib/Serializer"; import {EncryptorError} from "./lib/EncryptorError"; import {PhpSerializer} from "./serializers/phpSerializer"; import {JsonSerializer} from "./serializers/jsonSerializer"; import cryptTypes from "crypto"; import {Encryptor} from "./Encryptor"; const valid_aes = [128, 256]; const default_aes = 256; let crypto; //Determining if crypto support is unavailable try { crypto = require('crypto'); } catch (e) { throw new EncryptorError(e.message); } /** * Base encryptor Class */ export class Base_encryptor { /** Cypher type */ protected algorithm: string; /** SECRET KEY Buffer */ protected readonly secret: any; /** AES 128/256 */ protected aes_mode = default_aes; /** valid key length in laravel aes-[128]-cbc aes-[256]-cbc */ protected valid_aes_modes = valid_aes; /** Bytes number crypto.randomBytes default 8 */ protected random_bytes = 8; /** serialize driver */ private serialize_driver: Serializer; /** default serialize lib */ protected default_serialize_mode = 'php'; /** constructor options */ protected options: { key?: string, key_length?: number, random_bytes?: number, serialize_mode?: 'json'|'php'|'custom' }; /** for test only */ private raw_decrypted: any; /** * Return new Encryptor * * @param options {key: string, key_length?: number } * @param driver */ constructor(options, driver?: Serializer) { this.options = Object.assign({}, {serialize_mode: this.default_serialize_mode}, options); this.setSerializerDriver(driver); this.setAlgorithm(); this.secret = Base_encryptor.prepareAppKey(this.options.key); this.random_bytes = this.options.random_bytes ? this.options.random_bytes : this.random_bytes; } /** * encryptIt * * @return Promise object {iv, value, mac} */ protected encryptIt(data: string): Promise<any> { return this .generate_iv() .then(this.createCypherIv()) .then(this.cipherIt(data)) .then(this.generateEncryptedObject()) } /** * encryptIt * * @param data * @return object {iv, value, mac} */ protected encryptItSync(data: string): any { const iv = this.generate_iv_sync(); const cipher = this.createCipher(iv); const value = Base_encryptor.cryptoUpdate(cipher, data); return this.generateEncryptedObject()({iv, value}) } /** * decryptIt * * @param encrypted */ protected decryptIt(encrypted: string): any{ let payload; try { payload = JSON.parse(encrypted); } catch (e) { Base_encryptor.throwError('Encryptor decryptIt cannot parse json') } //check hmac payload.mac with crypto.timingSafeEqual to prevent timing attacks if(! Base_encryptor.validPayload(payload)) Base_encryptor.throwError('The payload is invalid.'); if(! this.validMac(payload)) Base_encryptor.throwError('The MAC is invalid.'); const decipherIv = this.createDecipheriv(payload.iv); const decrypted = Base_encryptor.cryptoDecipher(payload, decipherIv); if(process.env.NODE_ENV === 'test') this.raw_decrypted = decrypted; return this.ifserialized_unserialize(decrypted) } /** * prepareAppKey * * @param key */ static prepareAppKey(key: string): Buffer{ if(! key) Base_encryptor.throwError('no app key given'); return Buffer.from(key, 'base64'); } /** * set serializer driver * * @param driver */ setSerializerDriver(driver?: any){ if(driver) { if(! Base_encryptor.validateSerializerDriver(driver)) Base_encryptor.throwError('validateSerializerDriver'); this.serialize_driver = new Serializer(new driver); this.options.serialize_mode = 'custom'; }else{ this.serialize_driver = new Serializer(this.pickSerializeDriver()); } } /** * validateSerializerDriver * * @param driver */ static validateSerializerDriver(driver: any){ try { const custom_driver = new driver; return Base_encryptor .validateSerializerImplementsSerializerInterface(custom_driver) }catch (e) { Base_encryptor.throwError('validateSerializerDriver') } } /** * validateSerializerDriver * * @param driver */ static validateSerializerImplementsSerializerInterface(driver: any){ return (typeof driver['serialize'] === 'function') && (typeof driver['unSerialize'] === 'function') } /** * pickSerializeDriver */ pickSerializeDriver(){ if(! this.options.serialize_mode) this.options.serialize_mode ='php'; switch (this.options.serialize_mode) { case 'json': { return new JsonSerializer; } case 'php': { return new PhpSerializer; } default: { throw new EncryptorError( `Serializer Encryptor Class unknown option ${this.options.serialize_mode} serialize_mode` ) } } } /** * setAlgorithm * will populate this.algorithm with valid one aes-[128]-cbc aes-[256]-cbc * from options.aes_mode or this.key_length * * if there is an error will push it to errors (and return as reject at public methods) */ protected setAlgorithm() { if (this.options.key_length && this.valid_aes_modes.indexOf(this.options.key_length) < 0) Base_encryptor.throwError( 'The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.' ); this.algorithm = this.options.key_length ? `aes-${this.options.key_length}-cbc` : `aes-${this.aes_mode}-cbc`; } /** * Prepare Data * will receive data from this.encrypt(data) * and check if is a number to convert to string, * return data serialized if need it * * @param data * @param force_serialize */ protected prepareDataToCipher(data: any, force_serialize?: boolean): string{ if(force_serialize === true && this.serialize_driver.getDriverName() === 'PhpSerializer') { return this.serialize_driver.serialize(data); } data = Base_encryptor.ifNumberToString(data); return this.ifObjectToString(data); } /** * prepareDataToDecipher * will parse base64 to utf8 * @param data */ protected prepareDataToDecipher(data: any): string{ return Base_encryptor.base64ToUtf8(data); } /** * validPayload * * @param payload */ static validPayload(payload: any): boolean{ return payload.hasOwnProperty('iv') && payload.hasOwnProperty('value') && payload.hasOwnProperty('mac') && Buffer.from(payload.iv,'base64').toString('hex').length === 32; } /** * validMac * * @param payload */ validMac(payload: any): boolean{ try { const calculated = this.hashIt(payload.iv, payload.value); return crypto.timingSafeEqual(Buffer.from(calculated), Buffer.from(payload.mac)) }catch (e) { return false; } } /** * crypto update + final * * @param cipher * @param data */ static cryptoUpdate(cipher: cryptTypes.Cipher, data: string){ try{ return cipher.update(data, 'utf8', 'base64') + cipher.final('base64'); } catch (e) { Base_encryptor.throwError(e.message); } } /** * Create node crypto cipher Iv * * @param iv */ protected createCipher(iv: string): cryptTypes.Cipher{ try { return crypto.createCipheriv(this.algorithm, this.secret, iv); } catch (e) { Base_encryptor.throwError(e.message); } } /** * crypto createCipheriv * * @return Promise crypto cipher */ protected createCypherIv(): any { return (iv) => { return {iv, cipher: this.createCipher(iv)}; } } /** * generate a la Laravel Encrypted Object * * @param data */ protected cipherIt(data: string): any { return ({iv, cipher}: any) => { return { iv, value: Base_encryptor.cryptoUpdate(cipher, data) } } } /** * Generate 8 bytes IV * * @return Promise [16 hexadecimal string] */ protected generate_iv(): Promise<string> { return new Promise((resolve, reject) => { crypto.randomBytes(this.random_bytes, (err, buffer) => { if (err) return reject(err); resolve(buffer.toString('hex')) }); }); } /** * Generate 8 bytes IV * * @return string [16 hexadecimal string] */ protected generate_iv_sync(): string { try { const buf = crypto.randomBytes(this.random_bytes); return buf.toString('hex'); }catch (e) { Base_encryptor.throwError('generate_iv_sync error generating random bytes'); } } /** * generate Laravel Encrypted Object */ protected generateEncryptedObject() { return ({iv, value}: any) => { iv = Base_encryptor.toBase64(iv); return { iv, value, mac: this.hashIt(iv, value) }; } } /** * crypto createDecipheriv * * @param iv * @return crypto decipher */ protected createDecipheriv(iv: string): cryptTypes.Decipher { try { return crypto.createDecipheriv(this.algorithm, this.secret, Buffer.from(iv, 'base64')); }catch (e) { Base_encryptor.throwError(e.message); } } /** * cryptoDecipher * * @param payload * @param decipher */ static cryptoDecipher(payload: {iv,value,mac}, decipher: cryptTypes.Decipher) { try { return decipher.update(payload.value, 'base64', 'utf8') + decipher.final('utf8'); }catch (e) { Base_encryptor.throwError(e.message); } } /** * ifserialized_unserialize * * @param decrypted */ protected ifserialized_unserialize(decrypted: string): any { return this.serialize_driver.unSerialize(decrypted) } /** * Create HMAC hash a la laravel * * @param iv * @param encrypted * @return hex string */ protected hashIt(iv: string, encrypted: string): string { try{ const hmac = Base_encryptor.createHmac("sha256", this.secret); return hmac .update(Base_encryptor.setHmacPayload(iv, encrypted)) .digest("hex"); }catch (e) { Base_encryptor.throwError(e.message); } } /** * serialize data * * @param data * @return serialized data */ protected serialize(data: any): string { return this.serialize_driver.serialize(data) } /** * Unserialize data * * @param data * @return unserialized data */ protected unserialize(data: string): any { return this.serialize_driver.unSerialize(data) } /** * Convert data to base64 * * @param data * @return base64 string */ static toBase64(data): string { return Buffer.from(data).toString('base64'); } /** * Parse base64 to utf8 * * @param data * @return utf8 string */ static base64ToUtf8(data: string): string { if(typeof data !== 'string') throw new EncryptorError('base64ToUtf8 Error data arg not a string'); return Buffer.from(data, 'base64').toString('utf8'); } /** * Create crypto Hmac * * @param alg * @param secret */ static createHmac(alg: string, secret: Buffer): cryptTypes.Hmac { try{ return crypto.createHmac(alg, secret); } catch (e) { Base_encryptor.throwError(e.message); } } /** * Set hmac payload * * @param iv * @param encrypted */ static setHmacPayload(iv: string, encrypted: string): Buffer { return Buffer.from(iv + encrypted, 'utf-8') } /** * stringifyAndBase64 * will json.stringify object {iv, value, mac} and base64 it * * @param encrypted {iv, value, mac} * @return string base64 */ static stringifyAndBase64(encrypted: {iv, value, mac}): string { const payload = JSON.stringify(encrypted); return Buffer.from(payload).toString('base64'); } /** * ifObjectToString serialize object * * @param data */ protected ifObjectToString(data: any): string{ return (typeof data === 'object') ? this.serialize(data) : data; } /** * number To String * if data is a number convert to string * * @param data */ static ifNumberToString(data: any): string{ return (typeof data === 'number') ? data + '' : data; } /** * Throw Error. * * @param error */ static throwError(error) { if(error.name === 'EncryptorError') throw error; throw new EncryptorError(error); } /** * Generate a random key for the application. * * @return string */ static generateRandomKey(length?: number): string { length = length ? length : default_aes; //laravel supports 128/256 if(valid_aes.indexOf(length) < 0){ console.error('valid options are: ', valid_aes); return; } try{ const buf = crypto.randomBytes(length/8); return buf.toString('base64'); }catch (e) { Base_encryptor.throwError(e.message); } } /** * For testing only */ getRawDecrypted(){ return this.raw_decrypted } }