User-facing desktop application for server.garden
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

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;
}