const sjcl = require("./vendor/sjcl") sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."](); const SUPPORTED_VERSION = 2; const bytesInAUint32 = 4; const aesBlockSizeInBytes = 16; const sha256OutputSizeInBytes = 256/8; const keySizeInBytes = 32; module.exports = { deriveFrom: (passphrase) => { const keyBytes = scryptKeyDerivation(passphrase) return { encryptString: (x) => encryptString(x, keyBytes), decryptToString: (x) => decryptToString(x, keyBytes), encryptBytes: (x) => encryptBytes(x, keyBytes), decryptToBytes: (x) => decryptToBytes(x, keyBytes), } } } const scryptKeyDerivation = (passphrase) => { if(passphrase == "") { return new Uint8Array(keySizeInBytes) } const passphraseBitArray = sjcl.codec.bytes.toBits(stringToUtf8Bytes(passphrase)) const saltBytes = stringToUtf8Bytes("kennedy indicated notice experience zinc ot fountain feelings championship") const saltBitArray = sjcl.hash.sha256.hash(sjcl.codec.bytes.toBits(saltBytes)) const cpuAndMemoryCostToDerive = 1 << 16 const keySizeInBits = keySizeInBytes*8; const keyBitArray = sjcl.misc.scrypt(passphraseBitArray, saltBitArray, cpuAndMemoryCostToDerive, 8, 1, keySizeInBits) return sjcl.codec.bytes.fromBits(keyBitArray) } const encryptString = (plaintextString, keyBytes) => { if ( typeof plaintextString != "string" ) { throw new Error("encryptString() must be passed a string") } return encryptBytes(stringToUtf8Bytes(plaintextString), keyBytes) } const decryptToString = (serializedEncryptedBytes, keyBytes) => { return utf8BytesToString(decryptToBytes(serializedEncryptedBytes, keyBytes)); } const decryptToBytes = (serializedEncryptedBytes, keyBytes) => { let encrypted; try { encrypted = parseEncryptedBytes(serializedEncryptedBytes) } catch (err) { throw new Error(strings.errorMessage_FormatErrorX(err)); } initializationVectorAndCiphertext = new Uint8Array(encrypted.initializationVector.length+encrypted.ciphertext.length); initializationVectorAndCiphertext.set(encrypted.initializationVector); initializationVectorAndCiphertext.set(encrypted.ciphertext, encrypted.initializationVector.length); const hmac = new sjcl.misc.hmac(sjcl.codec.bytes.toBits(keyBytes), sjcl.hash.sha256); hmac.update(sjcl.codec.bytes.toBits(initializationVectorAndCiphertext)) const messageAuthenticationCode = sjcl.codec.bytes.fromBits(hmac.digest()); if(!uint8ArrayEquals(messageAuthenticationCode, encrypted.messageAuthenticationCode)) { const wrongPassphraseError = new Error(strings.errorMessage_WrongPassphraseError); wrongPassphraseError.wrongPassphrase = true; throw wrongPassphraseError; } return sjcl.codec.bytes.fromBits( sjcl.mode.cbc.decrypt( new sjcl.cipher.aes(sjcl.codec.bytes.toBits(keyBytes)), sjcl.codec.bytes.toBits(encrypted.ciphertext), sjcl.codec.bytes.toBits(encrypted.initializationVector) ) ); } const encryptBytes = (plaintextBytes, keyBytes) => { if ( !(plaintextBytes instanceof Uint8Array) ) { throw new Error("encryptBytes() must be passed a UInt8Array") } let initializationVector = getRandomBytes(aesBlockSizeInBytes); let hashOfPlaintext = sjcl.codec.bytes.fromBits(sjcl.hash.sha256.hash(sjcl.codec.bytes.toBits(plaintextBytes))) for(let i = 0; i < initializationVector.length; i++) { initializationVector[i] = initializationVector[i] ^ hashOfPlaintext[i % hashOfPlaintext.length] } const ciphertextBytes = sjcl.codec.bytes.fromBits( sjcl.mode.cbc.encrypt( new sjcl.cipher.aes(sjcl.codec.bytes.toBits(keyBytes)), sjcl.codec.bytes.toBits(plaintextBytes), sjcl.codec.bytes.toBits(initializationVector) ) ); initializationVectorAndCiphertext = new Uint8Array(initializationVector.length + ciphertextBytes.length); initializationVectorAndCiphertext.set(initializationVector); initializationVectorAndCiphertext.set(ciphertextBytes, initializationVector.length); const hmac = new sjcl.misc.hmac(sjcl.codec.bytes.toBits(keyBytes), sjcl.hash.sha256); hmac.update(sjcl.codec.bytes.toBits(initializationVectorAndCiphertext)) const messageAuthenticationCode = sjcl.codec.bytes.fromBits(hmac.digest()); return serializeEncryptedBytes(initializationVector, ciphertextBytes, messageAuthenticationCode) } function serializeEncryptedBytes(initializationVector, ciphertext, messageAuthenticationCode) { const versionNumber = SUPPORTED_VERSION; const totalLength = (bytesInAUint32 * 4) + initializationVector.length + ciphertext.length + messageAuthenticationCode.length; const buffer = new ArrayBuffer(totalLength); const dataView = new DataView(buffer); let currentOffset = 0; dataView.setUint32(currentOffset, versionNumber, true); currentOffset += bytesInAUint32; const writeByteArray = (byteArray) => { dataView.setUint32(currentOffset, byteArray.length, true); currentOffset += bytesInAUint32; for(let i = 0; i < byteArray.length; i++) { dataView.setUint8(currentOffset + i, byteArray[i], true) } currentOffset += byteArray.length; } writeByteArray(initializationVector); writeByteArray(ciphertext); writeByteArray(messageAuthenticationCode); return new Uint8Array(buffer); } function parseEncryptedBytes(serializedEncryptedBytes) { const toReturn = {}; //console.log(serializedEncryptedBytes.buffer); const dataView = new DataView( serializedEncryptedBytes.buffer, serializedEncryptedBytes.byteOffset, serializedEncryptedBytes.length ); let currentOffset = 0; const versionNumber = dataView.getUint32(currentOffset, true); currentOffset += bytesInAUint32; if(versionNumber != SUPPORTED_VERSION) { //console.log(serializedEncryptedBytes.toString('utf-8')) throw new Error(`unsupported serialization version number ${versionNumber}`) } const readByteArray = (name, expectedLength) => { const length = dataView.getUint32(currentOffset, true); currentOffset += bytesInAUint32; if(expectedLength > 0 && length != expectedLength) { throw new Error(`given ${name}Length (${length}) != expectedLength (${expectedLength})`) } toReturn[name] = new Uint8Array(length); for(let i = 0; i < length; i++) { toReturn[name][i] = dataView.getUint8(currentOffset + i, true) } currentOffset += length; } readByteArray("initializationVector", aesBlockSizeInBytes) readByteArray("ciphertext", -1) readByteArray("messageAuthenticationCode", sha256OutputSizeInBytes) return toReturn; } function stringToUtf8Bytes(str) { var utf8 = []; for (var i = 0; i < str.length; i++) { var charcode = str.charCodeAt(i); if (charcode < 0x80) utf8.push(charcode); else if (charcode < 0x800) { utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f)); } else if (charcode < 0xd800 || charcode >= 0xe000) { utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f)); } // surrogate pair else { i++; charcode = ((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff) utf8.push(0xf0 | (charcode >> 18), 0x80 | ((charcode >> 12) & 0x3f), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f)); } } return Uint8Array.from(utf8); } function utf8BytesToString(data) { // array of bytes var str = '', i; for (i = 0; i < data.length; i++) { var value = data[i]; if (value < 0x80) { str += String.fromCharCode(value); } else if (value > 0xBF && value < 0xE0) { str += String.fromCharCode((value & 0x1F) << 6 | data[i + 1] & 0x3F); i += 1; } else if (value > 0xDF && value < 0xF0) { str += String.fromCharCode((value & 0x0F) << 12 | (data[i + 1] & 0x3F) << 6 | data[i + 2] & 0x3F); i += 2; } else { // surrogate pair var charCode = ((value & 0x07) << 18 | (data[i + 1] & 0x3F) << 12 | (data[i + 2] & 0x3F) << 6 | data[i + 3] & 0x3F) - 0x010000; str += String.fromCharCode(charCode >> 10 | 0xD800, charCode & 0x03FF | 0xDC00); i += 3; } } return str; } // TODO if we ever port this to run in a browser, maybe we could add the sjcl prng in here as well. const getRandomBytes = ( (typeof self !== 'undefined' && (self.crypto || self.msCrypto)) ? function () { // Browsers const crypto = (self.crypto || self.msCrypto); return function (n) { const toReturn = new Uint8Array(n); crypto.getRandomValues(toReturn); return toReturn; }; } : function () { // NodeJS return require("crypto").randomBytes; } )(); function uint8ArrayEquals(a, b) { if (a.length != b.length) return false; for (var i = 0 ; i != a.length ; i++) { if (a[i] != b[i]) { return false; } } return true; }