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.

1179 lines
26 KiB

/*global require: false,
slowAES: false,
window: false,
exports: false,
rstr_hmac_sha256: false,
b64tohex: false,
hex2b64: false,
rstr_sha256: false,
Uint8Array: false,
escape: false,
unescape: false,
PBKDF2: false,
rstr_sha1: false,
Buffer: false,
RSAKey: false,
process: false */
/*jslint nomen: true */
// Simple symmetric and asymmetric crypto.
// Note: Keep an eye on http://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-02
var SHA1_SIZE = 20,
SHA256_SIZE = 32,
AES_BLOCK_SIZE = 16,
AES_128_KEY_SIZE = 16,
Crypt = function (parsed_key, options)
{
"use strict";
this.key = parsed_key;
options = options || {};
this.options = {};
this.options.json = options.json !== false;
this.options.check = options.check !== false;
this.options.pad = options.pad !== false;
this.options.custom = options.custom;
};
Crypt.make = function (key, options, cb)
{
"use strict";
if (!cb)
{
cb = options;
options = {};
}
if (!cb)
{
cb = key;
key = undefined;
}
if (typeof cb !== 'function')
{
options = cb;
cb = undefined;
}
var This = this,
crypt = new This(undefined, options);
This.parse_key(key, function (err, parsed_key)
{
if (err)
{
cb(err);
return;
}
crypt.key = parsed_key;
if (cb)
{
cb(null, crypt);
}
});
return crypt;
};
Crypt.get_version = function ()
{
"use strict";
return 1;
};
Crypt.get_key_size = function ()
{
"use strict";
return AES_128_KEY_SIZE;
};
Crypt.prototype.get_key = function ()
{
"use strict";
return this.key;
};
Crypt.prototype.check_version = function (data, f)
{
"use strict";
if (data.version > Crypt.get_version())
{
f.call(this, 'unsupported version');
return false;
}
return true;
};
Crypt.prototype.maybe_encrypt = function (arg_encrypt,
arg_data,
arg_f,
arg_get_key)
{
"use strict";
var ths = this, encrypt, data, f, get_key, get_key_data,
encrypted = function (err, edata, key_data)
{
if (err)
{
f.call(this, err);
}
else
{
f.call(this, null, { encrypted: true, data: edata, key_data: key_data });
}
},
not_encrypted = function ()
{
f.call(this, null, { encrypted: false, data: data });
};
if (typeof arg_data === 'function')
{
get_key_data = Array.prototype.slice.call(arguments, 3);
get_key = arg_f;
f = arg_data;
data = arg_encrypt;
encrypt = (get_key !== undefined) || this.key;
}
else
{
get_key_data = Array.prototype.slice.call(arguments, 4);
get_key = arg_get_key;
f = arg_f;
data = arg_data;
encrypt = arg_encrypt;
}
if (encrypt)
{
if (get_key !== undefined)
{
get_key_data.push(function (err, key, key_data, iv)
{
if (err)
{
f.call(ths, err);
}
else if (key)
{
Crypt.make(key, ths.options, function (err, crypt)
{
if (err)
{
f.call(ths, err);
}
else
{
crypt.encrypt(data, iv, function (err, data)
{
encrypted.call(this, err, data, key_data);
});
}
});
}
else
{
not_encrypted.call(ths);
}
});
get_key.apply(this, get_key_data);
}
else
{
this.encrypt(data, encrypted);
}
}
else
{
not_encrypted.call(this);
}
};
Crypt.prototype.maybe_decrypt = function (data, f, get_key)
{
"use strict";
if (data.encrypted)
{
if (get_key !== undefined)
{
var ths = this,
get_key_data = Array.prototype.slice.call(arguments, 3);
get_key_data.push(function (err, key)
{
if (err)
{
f.call(ths, err);
}
else
{
Crypt.make(key, ths.options, function (err, crypt)
{
if (err)
{
f.call(ths, err);
}
else
{
crypt.decrypt(data.data, f);
}
});
}
});
get_key_data.push(data.key_data);
get_key.apply(this, get_key_data);
}
else
{
this.decrypt(data.data, f);
}
}
else
{
f.call(this, null, data.data);
}
};
Crypt.prototype.maybe_sign = function (arg_sign, arg_data, arg_f, arg_get_key)
{
"use strict";
var ths = this, sign, data, f, get_key, get_key_data,
signed = function (err, sdata, key_data)
{
if (err)
{
f.call(this, err);
}
else
{
f.call(this, null, { signed: true, data: sdata, key_data: key_data });
}
},
not_signed = function ()
{
f.call(this, null, { signed: false, data: data });
};
if (typeof arg_data === 'function')
{
get_key_data = Array.prototype.slice.call(arguments, 3);
get_key = arg_f;
f = arg_data;
data = arg_sign;
sign = (get_key !== undefined) || this.key;
}
else
{
get_key_data = Array.prototype.slice.call(arguments, 4);
get_key = arg_get_key;
f = arg_f;
data = arg_data;
sign = arg_sign;
}
if (sign)
{
if (get_key !== undefined)
{
get_key_data.push(function (err, key, key_data)
{
if (err)
{
f.call(ths, err);
}
else if (key)
{
Crypt.make(key, ths.options, function (err, crypt)
{
if (err)
{
f.call(ths, err);
}
else
{
crypt.sign(data, function (err, data)
{
signed.call(this, err, data, key_data);
});
}
});
}
else
{
not_signed.call(ths);
}
});
get_key.apply(this, get_key_data);
}
else
{
this.sign(data, signed);
}
}
else
{
not_signed.call(this);
}
};
Crypt.prototype.maybe_verify = function (data, f, get_key)
{
"use strict";
if (data.signed)
{
if (get_key !== undefined)
{
var ths = this,
get_key_data = Array.prototype.slice.call(arguments, 3);
get_key_data.push(function (err, key)
{
if (err)
{
f.call(ths, err);
}
else
{
Crypt.make(key, ths.options, function (err, crypt)
{
if (err)
{
f.call(ths, err);
}
else
{
crypt.verify(data.data, f);
}
});
}
});
get_key_data.push(data.key_data);
get_key.apply(this, get_key_data);
}
else
{
this.verify(data.data, f);
}
}
else
{
f.call(this, null, data.data);
}
};
Crypt.sign_encrypt_sign = function (signing_key, encryption_key, data, iv, f)
{
"use strict";
if (!f)
{
f = iv;
iv = null;
}
var This = this;
This.make(signing_key, function (err, signer)
{
if (err)
{
f(err);
return;
}
This.make(encryption_key, function (err, encrypter)
{
if (err)
{
f(err);
return;
}
signer.sign(data, function (err, sv)
{
if (err)
{
f(err);
return;
}
encrypter.encrypt(sv, iv, function (err, ev)
{
if (err)
{
f(err);
return;
}
signer.sign(ev, f);
});
});
});
});
};
Crypt.verify_decrypt_verify = function (decryption_key, verifying_key, data, f)
{
"use strict";
var This = this;
This.make(verifying_key, function (err, verifier)
{
if (err)
{
f(err);
return;
}
This.make(decryption_key, function (err, decrypter)
{
if (err)
{
f(err);
return;
}
verifier.verify(data, function (err, vv)
{
if (err)
{
f(err);
return;
}
decrypter.decrypt(vv, function (err, dv)
{
if (err)
{
f(err);
return;
}
verifier.verify(dv, f);
});
});
});
});
};
var SlowCrypt;
if (typeof require === 'function')
{
var crypto = require('crypto'),
ursa = require('ursa'),
workaround = process.version.lastIndexOf('v0.8', 0) === 0;
Crypt.parse_key = function (key, cb)
{
"use strict";
if ((typeof key === 'string') &&
(key.lastIndexOf('-----BEGIN', 0) === 0))
{
if (key.indexOf('PUBLIC KEY') > 0)
{
key = ursa.createPublicKey(key, 'utf8');
}
else if (key.indexOf('PRIVATE KEY') > 0)
{
key = ursa.createPrivateKey(key, '', 'utf8');
}
cb(null, key);
}
else if (key && key.password)
{
var salt = key.salt || crypto.randomBytes(SHA1_SIZE),
hash;
if (salt.length < SHA1_SIZE)
{
hash = crypto.createHash('sha1');
hash.update(salt);
salt = hash.digest();
}
crypto.pbkdf2(key.password, salt, key.iterations, AES_128_KEY_SIZE,
function (err, derived_key)
{
if (err)
{
cb(err);
return;
}
if (key.progress)
{
key.progress(100);
}
cb(null,
{
key: derived_key,
salt: salt
});
});
}
else
{
cb(null, key);
}
};
Crypt.prototype.stringify = function (data)
{
"use strict";
return this.options.json ? new Buffer(JSON.stringify(data), 'utf8') : data;
};
Crypt.prototype.parse = function (data)
{
"use strict";
return this.options.json ? JSON.parse(data.toString('utf8')) : data;
};
Crypt.prototype.encrypt = function (data, iv, f)
{
"use strict";
var key, ekey, iv64, cipher, jdata, edata = '',
encoding = workaround ? 'hex' : 'base64';
try
{
if (!f)
{
f = iv;
iv = null;
}
if (this.key.privateEncrypt)
{
f.call(this, "can't encrypt using private key");
return;
}
if (this.key.encrypt)
{
key = crypto.randomBytes(AES_128_KEY_SIZE);
ekey = this.key.encrypt(key, undefined, 'base64', ursa.RSA_PKCS1_OAEP_PADDING);
}
else
{
key = this.key.key || this.key;
}
iv = iv || crypto.randomBytes(AES_BLOCK_SIZE);
iv64 = iv.toString('base64');
cipher = crypto.createCipheriv('AES-128-CBC', key, iv);
cipher.setAutoPadding(this.options.pad);
jdata = this.stringify(data);
if (this.options.check)
{
edata = cipher.update(crypto.createHash('sha256')
.update(jdata)
.digest(), null, encoding);
}
edata += cipher.update(jdata, null, encoding);
edata += cipher.final(encoding);
if (workaround)
{
edata = new Buffer(edata, encoding).toString('base64');
}
}
catch (ex)
{
f.call(this, ex);
return;
}
f.call(this, null, { iv: iv64, data: edata, ekey: ekey, version: Crypt.get_version() });
};
Crypt.prototype.decrypt = function (data, f)
{
"use strict";
var key, decipher, ddata, jdata;
try
{
if (!this.check_version(data, f))
{
return;
}
if (this.key.decrypt)
{
key = this.key.decrypt(data.ekey, 'base64', undefined, ursa.RSA_PKCS1_OAEP_PADDING);
}
else if (!this.key.publicDecrypt)
{
key = this.key.key || this.key;
}
else
{
f.call(this, "can't decrypt using public key");
return;
}
decipher = crypto.createDecipheriv(
'AES-128-CBC',
key,
new Buffer(data.iv, 'base64'));
decipher.setAutoPadding(this.options.pad);
ddata = decipher.update(data.data, 'base64');
if (workaround)
{
ddata = new Buffer(ddata + decipher.final(), 'binary');
}
else
{
ddata = Buffer.concat([ddata, decipher.final()]);
}
if (this.options.check)
{
jdata = ddata.slice(SHA256_SIZE);
if (crypto.createHash('sha256')
.update(jdata)
.digest('base64') !== ddata.toString('base64', 0, SHA256_SIZE))
{
f.call(this, 'digest mismatch');
return;
}
}
else
{
jdata = ddata;
}
jdata = this.parse(jdata);
}
catch (ex)
{
f.call(this, ex);
return;
}
f.call(this, null, jdata);
};
Crypt.prototype.sign = function (data, f)
{
"use strict";
var jdata, signature;
try
{
jdata = this.stringify(data);
if (this.key.hashAndSign)
{
signature = this.key.hashAndSign('sha256', jdata, null, 'base64', ursa.RSA_PKCS1_PSS_PADDING);
}
else if (!this.key.encrypt)
{
signature = crypto.createHmac('sha256', this.key.key || this.key)
.update(jdata)
.digest('base64');
}
else
{
f.call(this, "can't sign using public key");
return;
}
jdata = jdata.toString('base64');
}
catch (ex)
{
f.call(this, ex);
return;
}
f.call(this, null, { data: jdata, signature: signature, version: Crypt.get_version() });
};
Crypt.prototype.verify = function (data, f)
{
"use strict";
var match, ddata, jdata;
try
{
if (!this.check_version(data, f))
{
return;
}
ddata = new Buffer(data.data, 'base64');
if (this.key.decrypt)
{
f.call(this, "can't verify using private key");
return;
}
if (this.key.hashAndVerify)
{
match = this.key.hashAndVerify(
'sha256',
data.data,
data.signature,
'base64',
ursa.RSA_PKCS1_PSS_PADDING);
}
else
{
match = crypto.createHmac('sha256', this.key.key || this.key)
.update(ddata)
.digest('base64') === data.signature;
}
if (match)
{
jdata = this.parse(ddata);
}
else
{
f.call(this, 'digest mismatch');
return;
}
}
catch (ex)
{
f.call(this, ex);
return;
}
f.call(this, null, jdata);
};
SlowCrypt = function ()
{
"use strict";
Crypt.apply(this, arguments);
};
SlowCrypt.make = Crypt.make;
SlowCrypt.get_version = Crypt.get_version;
SlowCrypt.get_key_size = Crypt.get_key_size;
SlowCrypt.sign_encrypt_sign = Crypt.sign_encrypt_sign;
SlowCrypt.verify_decrypt_verify = Crypt.verify_decrypt_verify;
SlowCrypt.prototype = Object.create(Crypt.prototype);
}
else
{
SlowCrypt = Crypt;
}
var get_char_codes = function(s)
{
"use strict";
var r = [], i;
for (i = 0; i < s.length; i += 1)
{
r.push(s.charCodeAt(i));
}
return r;
};
SlowCrypt.parse_key = function (key, cb)
{
"use strict";
var rsa_key, salt, pbkdf2;
if (typeof key === 'string')
{
if (key.lastIndexOf('-----BEGIN', 0) === 0)
{
if (key.indexOf('PUBLIC KEY') > 0)
{
rsa_key = new RSAKey();
rsa_key.readPublicKeyFromPEMString(key);
cb(null, rsa_key);
return;
}
if (key.indexOf('PRIVATE KEY') > 0)
{
rsa_key = new RSAKey();
rsa_key.readPrivateKeyFromPEMString(key);
cb(null, rsa_key);
return;
}
}
cb(null, get_char_codes(key));
}
else if (key && key.password)
{
salt = key.salt;
if (!salt)
{
salt = new Uint8Array(SHA1_SIZE);
window.crypto.getRandomValues(salt);
salt = String.fromCharCode.apply(String, Array.prototype.slice.call(salt));
}
if (salt.length < SHA1_SIZE)
{
salt = rstr_sha1(salt);
}
pbkdf2 = new PBKDF2(key.password,
salt,
key.iterations,
AES_128_KEY_SIZE);
pbkdf2.deriveKey(
key.progress || function () { return undefined; },
function (derived_key)
{
var bytes = [], i;
for (i = 0; i < derived_key.length; i += 2)
{
bytes.push(parseInt(derived_key.substr(i, 2), 16));
}
cb(null,
{
key: bytes,
salt: salt
});
});
}
else
{
cb(null, key);
}
};
SlowCrypt.prototype.stringify = function (data)
{
"use strict";
// http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
return this.options.json ? unescape(encodeURIComponent(JSON.stringify(data))) : data;
};
SlowCrypt.prototype.parse = function (data)
{
"use strict";
return this.options.json ? JSON.parse(decodeURIComponent(escape(data))) : data;
};
SlowCrypt.prototype.encrypt = function (data, iv, f)
{
"use strict";
var key_arr, ekey, iv64, jdata, edata;
try
{
if (!f)
{
f = iv;
iv = null;
}
if (this.key.isPrivate)
{
f.call(this, "can't encrypt using private key");
return;
}
if (this.key.isPublic)
{
key_arr = new Uint8Array(AES_128_KEY_SIZE);
window.crypto.getRandomValues(key_arr);
ekey = hex2b64(this.key.encryptOAEP(String.fromCharCode.apply(String, Array.prototype.slice.call(key_arr))/*, rstr_sha256, 32*/));
}
else
{
key_arr = this.key.key || this.key;
}
if (!iv)
{
iv = new Uint8Array(AES_BLOCK_SIZE);
window.crypto.getRandomValues(iv);
}
iv64 = window.btoa(String.fromCharCode.apply(String, Array.prototype.slice.call(iv)));
jdata = this.stringify(data);
if (!this.options.pad)
{
slowAES._padBytesIn_save = slowAES.padBytesIn;
slowAES.padBytesIn = function (data)
{
return data;
};
}
if (this.options.check)
{
if (typeof jdata !== 'string')
{
jdata = String.fromCharCode.apply(String, jdata);
}
jdata = get_char_codes(rstr_sha256(jdata) + jdata);
}
else if (typeof jdata === 'string')
{
jdata = get_char_codes(jdata);
}
try
{
edata = slowAES.encrypt(
jdata,
slowAES.modeOfOperation.CBC,
key_arr,
iv);
}
finally
{
if (!this.options.pad)
{
slowAES.padBytesIn = slowAES._padBytesIn_save;
}
}
edata = window.btoa(String.fromCharCode.apply(String, edata));
}
catch (ex)
{
f.call(this, ex);
return;
}
f.call(this, null, { iv: iv64, data: edata, ekey: ekey, version: SlowCrypt.get_version() });
};
SlowCrypt.prototype.decrypt = function (data, f)
{
"use strict";
var key_arr, ddata, jdata;
try
{
if (!this.check_version(data, f))
{
return;
}
if (this.key.isPublic)
{
f.call(this, "can't decrypt using public key");
return;
}
if (this.key.isPrivate)
{
key_arr = get_char_codes(this.key.decryptOAEP(b64tohex(data.ekey)/*, rstr_sha256, 32*/));
}
else
{
key_arr = this.key.key || this.key;
}
if (!this.options.pad)
{
slowAES._unpadBytesOut_save = slowAES.unpadBytesOut;
slowAES.unpadBytesOut = function (data)
{
return data;
};
}
try
{
ddata = String.fromCharCode.apply(String, slowAES.decrypt(
get_char_codes(window.atob(data.data)),
slowAES.modeOfOperation.CBC,
key_arr,
get_char_codes(window.atob(data.iv))));
}
finally
{
if (!this.options.pad)
{
slowAES.unpadBytesOut = slowAES._unpadBytesOut_save;
}
}
if (this.options.check)
{
jdata = ddata.substr(SHA256_SIZE);
if (rstr_sha256(jdata) !== ddata.substr(0, SHA256_SIZE))
{
f.call(this, 'digest mismatch');
return;
}
}
else
{
jdata = ddata;
}
jdata = this.parse(jdata);
}
catch (ex)
{
f.call(this, ex);
return;
}
f.call(this, null, jdata);
};
SlowCrypt.prototype.sign = function (data, f)
{
"use strict";
var jdata, signature;
try
{
if (this.key.isPublic)
{
f.call(this, "can't sign using public key");
return;
}
jdata = this.stringify(data);
if (typeof jdata !== 'string')
{
jdata = String.fromCharCode.apply(String, jdata);
}
if (this.key.isPrivate)
{
signature = hex2b64(this.key.signPSS(jdata, 'sha256'));
}
else
{
signature = window.btoa(rstr_hmac_sha256(String.fromCharCode.apply(
String, this.key.key || this.key), jdata));
}
jdata = window.btoa(jdata);
}
catch (ex)
{
f.call(this, ex);
return;
}
f.call(this, null, { data: jdata, signature: signature, version: SlowCrypt.get_version() });
};
SlowCrypt.prototype.verify = function (data, f)
{
"use strict";
var match, ddata, jdata;
try
{
if (!this.check_version(data, f))
{
return;
}
ddata = window.atob(data.data);
if (this.key.isPrivate)
{
f.call(this, "can't verify using private key");
return;
}
if (this.key.isPublic)
{
match = this.key.verifyPSS(ddata, b64tohex(data.signature), 'sha256');
}
else
{
match = window.btoa(rstr_hmac_sha256(String.fromCharCode.apply(
String, this.key.key || this.key), ddata)) ===
data.signature;
}
if (match)
{
jdata = this.parse(ddata);
}
else
{
f.call(this, 'digest mismatch');
return;
}
}
catch (ex)
{
f.call(this, ex);
return;
}
f.call(this, null, jdata);
};
if (typeof exports === 'object')
{
exports.Crypt = Crypt;
exports.SlowCrypt = SlowCrypt;
}