You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
8.7 KiB
261 lines
8.7 KiB
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; |
|
} |
|
|
|
|
|
|