refactoring of crypto worker code and lots of cleanup

This commit is contained in:
Tankred Hase 2013-06-05 01:47:28 +02:00
parent 622e787ba7
commit f2a14ad65b
8 changed files with 99 additions and 543 deletions

View File

@ -4,7 +4,6 @@
"jquery": true,
"node": true,
"browser": true,
"camelcase": true,
"nonew": true,
"curly": true,
"eqeqeq": true,
@ -32,10 +31,12 @@
"describe",
"it",
"chai",
"asyncTest",
"ok",
"equal",
"deepEqual",
"start",
"TestData",
"chrome"
],

View File

@ -7,7 +7,7 @@
var CryptoBatch = function(aes, rsa, util, _) {
/**
* Encrypt and sign a an item using AES and RSA
* Encrypt and sign an item using AES and RSA
* @param i [Object] The item to encrypt
* @param receiverPubkey [String] The public key used to encrypt
* @param senderKeyId [String] The sender's private key ID used to sign

View File

@ -29,7 +29,11 @@ app.crypto.Crypto = function(window, util) {
self.rsaKeySize = args.rsaKeySize;
// derive PBKDF2 from password in web worker thread
self.deriveKey(args.password, self.keySize, function(pbkdf2) {
self.deriveKey(args.password, self.keySize, function(err, pbkdf2) {
if (err) {
callback(err);
return;
}
// check if key exists
if (!args.storedKeypair) {
@ -106,29 +110,13 @@ app.crypto.Crypto = function(window, util) {
* Do PBKDF2 key derivation in a WebWorker thread
*/
this.deriveKey = function(password, keySize, callback) {
// check for WebWorker support
if (window.Worker) {
// init webworker thread
var worker = new Worker(app.config.workerPath + '/crypto/pbkdf2-worker.js');
worker.onmessage = function(e) {
// return derived key from the worker
callback(e.data);
};
// send plaintext data to the worker
worker.postMessage({
password: password,
keySize: keySize
});
} else {
// no WebWorker support... do synchronous call
startWorker('/crypto/pbkdf2-worker.js', {
password: password,
keySize: keySize
}, callback, function() {
var pbkdf2 = new app.crypto.PBKDF2();
var key = pbkdf2.getKey(password, keySize);
callback(key);
}
return pbkdf2.getKey(password, keySize);
});
};
//
@ -136,43 +124,29 @@ app.crypto.Crypto = function(window, util) {
//
this.aesEncrypt = function(plaintext, key, iv, callback) {
if (window.Worker) {
var self = this;
var worker = new Worker(app.config.workerPath + '/crypto/aes-worker.js');
worker.onmessage = function(e) {
callback(e.data);
};
worker.postMessage({
type: 'encrypt',
plaintext: plaintext,
key: key,
iv: iv
});
} else {
var ct = this.aesEncryptSync(plaintext, key, iv);
callback(ct);
}
startWorker('/crypto/aes-worker.js', {
type: 'encrypt',
plaintext: plaintext,
key: key,
iv: iv
}, callback, function() {
return self.aesEncryptSync(plaintext, key, iv);
});
};
this.aesDecrypt = function(ciphertext, key, iv, callback) {
if (window.Worker) {
var self = this;
var worker = new Worker(app.config.workerPath + '/crypto/aes-worker.js');
worker.onmessage = function(e) {
callback(e.data);
};
worker.postMessage({
type: 'decrypt',
ciphertext: ciphertext,
key: key,
iv: iv
});
} else {
var pt = this.aesDecryptSync(ciphertext, key, iv);
callback(pt);
}
startWorker('/crypto/aes-worker.js', {
type: 'decrypt',
ciphertext: ciphertext,
key: key,
iv: iv
}, callback, function() {
return self.aesDecryptSync(ciphertext, key, iv);
});
};
this.aesEncryptSync = function(plaintext, key, iv) {
@ -188,41 +162,23 @@ app.crypto.Crypto = function(window, util) {
//
this.aesEncryptList = function(list, callback) {
if (window.Worker) {
var worker = new Worker(app.config.workerPath + '/crypto/aes-batch-worker.js');
worker.onmessage = function(e) {
callback(e.data);
};
worker.postMessage({
type: 'encrypt',
list: list
});
} else {
startWorker('/crypto/aes-batch-worker.js', {
type: 'encrypt',
list: list
}, callback, function() {
var batch = new cryptoLib.CryptoBatch(aes);
var encryptedList = batch.encryptList(list);
callback(encryptedList);
}
return batch.encryptList(list);
});
};
this.aesDecryptList = function(list, callback) {
if (window.Worker) {
var worker = new Worker(app.config.workerPath + '/crypto/aes-batch-worker.js');
worker.onmessage = function(e) {
callback(e.data);
};
worker.postMessage({
type: 'decrypt',
list: list
});
} else {
startWorker('/crypto/aes-batch-worker.js', {
type: 'decrypt',
list: list
}, callback, function() {
var batch = new cryptoLib.CryptoBatch(aes);
var decryptedList = batch.decryptList(list);
callback(decryptedList);
}
return batch.decryptList(list);
});
};
//
@ -258,24 +214,15 @@ app.crypto.Crypto = function(window, util) {
envelopes.push(envelope);
});
if (window.Worker) {
var worker = new Worker(app.config.workerPath + '/crypto/crypto-batch-worker.js');
worker.onmessage = function(e) {
callback(null, e.data);
};
worker.postMessage({
type: 'encrypt',
list: envelopes,
senderPrivkey: senderPrivkey,
receiverPubkeys: receiverPubkeys
});
} else {
startWorker('/crypto/crypto-batch-worker.js', {
type: 'encrypt',
list: envelopes,
senderPrivkey: senderPrivkey,
receiverPubkeys: receiverPubkeys
}, callback, function() {
var batch = new cryptoLib.CryptoBatch(aes, rsa, util, _);
var encryptedList = batch.encryptListForUser(envelopes, receiverPubkeys, senderPrivkey);
callback(null, encryptedList);
}
return batch.encryptListForUser(envelopes, receiverPubkeys, senderPrivkey);
});
};
this.decryptListForUser = function(list, senderPubkeys, callback) {
@ -292,24 +239,37 @@ app.crypto.Crypto = function(window, util) {
privateKey: keypair.privkeyPem
};
if (window.Worker) {
var worker = new Worker(app.config.workerPath + '/crypto/crypto-batch-worker.js');
worker.onmessage = function(e) {
callback(null, e.data);
};
worker.postMessage({
type: 'decrypt',
list: list,
receiverPrivkey: receiverPrivkey,
senderPubkeys: senderPubkeys
});
} else {
startWorker('/crypto/crypto-batch-worker.js', {
type: 'decrypt',
list: list,
receiverPrivkey: receiverPrivkey,
senderPubkeys: senderPubkeys
}, callback, function() {
var batch = new cryptoLib.CryptoBatch(aes, rsa, util, _);
var decryptedList = batch.decryptListForUser(list, senderPubkeys, receiverPrivkey);
callback(null, decryptedList);
}
return batch.decryptListForUser(list, senderPubkeys, receiverPrivkey);
});
};
function startWorker(script, args, callback, noWorker) {
// check for WebWorker support
if (window.Worker) {
// init webworker thread
var worker = new Worker(app.config.workerPath + script);
worker.onmessage = function(e) {
// return derived key from the worker
callback(null, e.data);
};
// send data to the worker
worker.postMessage(args);
} else {
// no WebWorker support... do synchronous call
var result = noWorker();
callback(null, result);
}
}
};

View File

@ -1,341 +0,0 @@
/**
* A wrapper for asymmetric OpenPGP encryption logic
*/
app.crypto.PGP = function(window, openpgp, util, server) {
'use strict';
var self = this,
privateKey, // user's private key
publicKey, // user's public key
passphrase; // user's passphrase used for decryption
openpgp.init(); // initialize OpenPGP.js
//
// Key management
//
/**
* Check if user already has a public key on the server and if not,
* generate a new keypait for the user
*/
self.initKeyPair = function(loginInfo, callback, displayCallback, finishCallback) {
// check if user already has a keypair in local storage
if (loginInfo.publicKeyId) {
// decode base 64 key ID
var keyId = window.atob(loginInfo.publicKeyId);
// read the user's keys from local storage
callback(keyId);
} else {
// user has no key pair yet
displayCallback(function() {
// generate new key pair with 2048 bit RSA keys
var keys = self.generateKeys(2048);
var keyId = keys.privateKey.getKeyId();
// display finish
finishCallback(keyId);
// read the user's keys from local storage
callback(keyId);
});
}
};
/**
* Generate a key pair for the user
* @param numBits [int] number of bits for the key creation. (should be 1024+, generally)
* @email [string] user's email address
* @pass [string] a passphrase used to protect the private key
*/
self.generateKeys = function(numBits) {
// check passphrase
if (!passphrase && passphrase !== '') {
throw 'No passphrase set!';
}
var userId = 'SafeWith.me User <anonymous@dunno.com>';
var keys = openpgp.generate_key_pair(1, numBits, userId, passphrase); // keytype 1=RSA
self.importKeys(keys.publicKeyArmored, keys.privateKeyArmored);
return keys;
};
/**
* Import the users key into the HTML5 local storage
*/
self.importKeys = function(publicKeyArmored, privateKeyArmored) {
// check passphrase
if (!passphrase && passphrase !== '') {
throw 'No passphrase set!';
}
// store keys in html5 local storage
openpgp.keyring.importPrivateKey(privateKeyArmored, passphrase);
openpgp.keyring.importPublicKey(publicKeyArmored);
openpgp.keyring.store();
};
/**
* Export the keys by using the HTML5 FileWriter
*/
self.exportKeys = function(callback) {
// build blob
var buf = util.binStr2ArrBuf(publicKey.armored + privateKey.armored);
var blob = util.arrBuf2Blob(buf, 'text/plain');
// create url
util.createUrl(undefined, blob, callback);
};
/**
* Read the users keys from the browser's HTML5 local storage
* @email [string] user's email address
* @keyId [string] the public key ID in unicode (not base 64)
*/
self.readKeys = function(keyId, callback, errorCallback) {
// read keys from keyring (local storage)
var privKeyQuery = openpgp.keyring.getPrivateKeyForKeyId(keyId)[0];
if (privKeyQuery) {
privateKey = privKeyQuery.key;
}
publicKey = openpgp.keyring.getPublicKeysForKeyId(keyId)[0];
// check keys
if (!publicKey || !privateKey || (publicKey.keyId !== privateKey.keyId)) {
// no amtching keys found in the key store
return false;
}
// read passphrase from local storage if no passphrase is specified
if (!passphrase && passphrase !== '') {
passphrase = window.sessionStorage.getItem(window.btoa(keyId) + 'Passphrase');
}
// check passphrase
if (!passphrase && passphrase !== '') {
return false;
}
// do test encrypt/decrypt to verify passphrase
try {
var testCt = self.asymmetricEncrypt('test');
self.asymmetricDecrypt(testCt);
} catch (e) {
return false;
}
return true;
};
/**
* Generate a new key pair for the user and persist the public key on the server
*/
self.syncKeysToServer = function(email, callback) {
// base64 encode key ID
var keyId = publicKey.keyId;
var encodedKeyId = window.btoa(keyId);
var pubKey = {
keyId: encodedKeyId,
ownerEmail: email,
asciiArmored: publicKey.armored
};
var privKey = {
keyId: encodedKeyId,
ownerEmail: email,
asciiArmored: privateKey.armored
};
var jsonPublicKey = JSON.stringify(pubKey);
var jsonPrivateKey = JSON.stringify(privKey);
// first upload public key
server.xhr({
type: 'POST',
uri: '/ws/publicKeys',
contentType: 'application/json',
expected: 201,
body: jsonPublicKey,
success: function(resp) {
uploadPrivateKeys();
},
error: function(e) {
// if server is not available, just continue
// and read the user's keys from local storage
console.log('Server unavailable: keys were not synced to server!');
callback(keyId);
}
});
// then upload private key
function uploadPrivateKeys() {
server.xhr({
type: 'POST',
uri: '/ws/privateKeys',
contentType: 'application/json',
expected: 201,
body: jsonPrivateKey,
success: function(resp) {
// read the user's keys from local storage
callback(keyId);
}
});
}
};
/**
* Get the keypair from the server and import them into localstorage
*/
self.fetchKeys = function(email, keyId, callback, errCallback) {
var base64Key = window.btoa(keyId);
var encodedKeyId = encodeURIComponent(base64Key);
// get public key
server.xhr({
type: 'GET',
uri: '/ws/publicKeys?keyId=' + encodedKeyId,
expected: 200,
success: function(pubKey) {
getPrivateKey(pubKey);
},
error: function(e) {
// if server is not available, just continue
console.log('Server unavailable: keys could not be fetched from server!');
errCallback(e);
}
});
// get private key
function getPrivateKey(pubKey) {
server.xhr({
type: 'GET',
uri: '/ws/privateKeys?keyId=' + encodedKeyId,
expected: 200,
success: function(privKey) {
// import keys
self.importKeys(pubKey.asciiArmored, privKey.asciiArmored, email);
callback({
privateKey: privKey,
publicKey: pubKey
});
}
});
}
};
/**
* Get the current user's private key
*/
self.getPrivateKey = function() {
if (!privateKey) {
return undefined;
}
return privateKey.armored;
};
/**
* Get the current user's public key
*/
self.getPublicKey = function() {
if (!publicKey) {
return undefined;
}
return publicKey.armored;
};
/**
* Get the current user's base64 encoded public key ID
*/
self.getPublicKeyIdBase64 = function() {
return window.btoa(publicKey.keyId);
};
/**
* Get the user's passphrase for decrypting their private key
*/
self.setPassphrase = function(pass) {
passphrase = pass;
};
/**
* Store the passphrase for the current session
*/
self.rememberPassphrase = function(keyId) {
var base64KeyId = window.btoa(keyId);
window.sessionStorage.setItem(base64KeyId + 'Passphrase', passphrase);
};
//
// Asymmetric crypto
//
/**
* Encrypt a string
* @param customPubKey [PublicKey] (optional) another user's public key for sharing
*/
self.asymmetricEncrypt = function(plaintext, customPubKey) {
var pub_key = null;
if (customPubKey) {
// use a custom set public for e.g. or sharing
pub_key = openpgp.read_publicKey(customPubKey);
} else {
// use the user's local public key
pub_key = openpgp.read_publicKey(publicKey.armored);
}
var ciphertext = openpgp.write_encrypted_message(pub_key, window.btoa(plaintext));
return ciphertext;
};
/**
* Decrypt a string
*/
self.asymmetricDecrypt = function(ciphertext) {
var priv_key = openpgp.read_privateKey(privateKey.armored);
var msg = openpgp.read_message(ciphertext);
var keymat = null;
var sesskey = null;
// Find the private (sub)key for the session key of the message
for (var i = 0; i < msg[0].sessionKeys.length; i++) {
if (priv_key[0].privateKeyPacket.publicKey.getKeyId() == msg[0].sessionKeys[i].keyId.bytes) {
keymat = {
key: priv_key[0],
keymaterial: priv_key[0].privateKeyPacket
};
sesskey = msg[0].sessionKeys[i];
break;
}
for (var j = 0; j < priv_key[0].subKeys.length; j++) {
if (priv_key[0].subKeys[j].publicKey.getKeyId() == msg[0].sessionKeys[i].keyId.bytes) {
keymat = {
key: priv_key[0],
keymaterial: priv_key[0].subKeys[j]
};
sesskey = msg[0].sessionKeys[i];
break;
}
}
}
if (keymat !== null) {
if (!keymat.keymaterial.decryptSecretMPIs(passphrase)) {
throw "Passphrase for secrect key was incorrect!";
}
var decrypted = msg[0].decrypt(keymat, sesskey);
return window.atob(decrypted);
} else {
throw "No private key found!";
}
};
};
/**
* This function needs to be implemented, since it is used by the openpgp utils
*/
function showMessages(str) {}

View File

@ -54,23 +54,23 @@
this.formatDate = function(date) {
var year = "" + date.getFullYear();
var month = "" + (date.getMonth() + 1);
if (month.length == 1) {
if (month.length === 1) {
month = "0" + month;
}
var day = "" + date.getDate();
if (day.length == 1) {
if (day.length === 1) {
day = "0" + day;
}
var hour = "" + date.getHours();
if (hour.length == 1) {
if (hour.length === 1) {
hour = "0" + hour;
}
var minute = "" + date.getMinutes();
if (minute.length == 1) {
if (minute.length === 1) {
minute = "0" + minute;
}
var second = "" + date.getSeconds();
if (second.length == 1) {
if (second.length === 1) {
second = "0" + second;
}
return year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second;

View File

@ -1,70 +0,0 @@
module("PGP Crypto");
var pgp_test = {
keyID: null,
keySize: 1024
};
asyncTest("Init", 1, function() {
// init dependencies
pgp_test.util = new app.crypto.Util(window);
pgp_test.crypto = new app.crypto.PGP(window, openpgp, util, null);
pgp_test.crypto.setPassphrase('asdf');
ok(pgp_test.crypto, 'PGP crypto');
pgp_test.helperEncrDecr = function(crypto, keyId, plaintext) {
if (!crypto.getPublicKey()) {
crypto.readKeys(keyId);
}
console.log('plaintext size [bytes]: ' + plaintext.length);
var startTime = (new Date).getTime();
var ct = crypto.asymmetricEncrypt(plaintext);
var diff = (new Date).getTime() - startTime;
console.log('Time taken for encryption [ms]: ' + diff);
ok(ct, "ciphertext: see console output for benchmark");
console.log('ciphertext size [bytes]: ' + ct.length);
var decrStart = (new Date).getTime();
var pt = crypto.asymmetricDecrypt(ct);
var decrDiff = (new Date).getTime() - decrStart;
console.log('Time taken for decryption [ms]: ' + decrDiff);
ok(pt, "decrypted: see console output for benchmark");
equal(pt, plaintext, "Decrypted should be the same as the plaintext");
};
start();
});
asyncTest("Generate keypair, De/Encrypt", 7, function() {
var startTime = (new Date).getTime();
var keys = pgp_test.crypto.generateKeys(pgp_test.keySize);
var diff = (new Date).getTime() - startTime;
pgp_test.keyID = keys.privateKey.getKeyId();
pgp_test.crypto.readKeys(pgp_test.keyID);
console.log('Time taken for key generation [ms]: ' + diff + ' (' + pgp_test.keySize + ' bit RSA keypair)');
ok(pgp_test.crypto.getPrivateKey());
ok(pgp_test.crypto.getPrivateKey().indexOf('-----BEGIN PGP PRIVATE KEY BLOCK-----') === 0);
ok(pgp_test.crypto.getPublicKey());
ok(pgp_test.crypto.getPublicKey().indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') === 0);
pgp_test.helperEncrDecr(pgp_test.crypto, pgp_test.keyID, '06a9214036b8a15b512e03d534120006');
start();
// pgp_test.crypto.exportKeys(function(url) {
// ok(url, 'export url');
//
// $.get(url, function(data) {
// ok(data.indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') !== -1, 'exportd public key');
// ok(data.indexOf('-----END PGP PRIVATE KEY BLOCK-----') !== -1, 'export private key');
//
// start();
// });
// });
});

View File

@ -1,3 +1,5 @@
'use strict';
module("Crypto Api");
var crypto_test = {
@ -40,30 +42,33 @@ asyncTest("Init with keypair", 1, function() {
rsaKeySize: crypto_test.rsaKeySize,
storedKeypair: crypto_test.generatedKeypair
}, function(err, generatedKeypair) {
ok(!err, 'Init crypto with keypair input');
ok(!err && !generatedKeypair, 'Init crypto with keypair input');
start();
});
});
asyncTest("PBKDF2 (Async/Worker)", 1, function() {
crypto_test.crypto.deriveKey(crypto_test.password, crypto_test.keySize, function(key) {
asyncTest("PBKDF2 (Async/Worker)", 2, function() {
crypto_test.crypto.deriveKey(crypto_test.password, crypto_test.keySize, function(err, key) {
ok(!err);
equal(crypto_test.util.base642Str(key).length * 8, crypto_test.keySize, 'Keysize ' + crypto_test.keySize);
start();
});
});
asyncTest("AES en/decrypt (Async/Worker)", 2, function() {
asyncTest("AES en/decrypt (Async/Worker)", 4, function() {
var secret = 'Big secret';
var key = crypto_test.util.random(crypto_test.keySize);
var iv = crypto_test.util.random(crypto_test.ivSize);
crypto_test.crypto.aesEncrypt(secret, key, iv, function(ciphertext) {
crypto_test.crypto.aesEncrypt(secret, key, iv, function(err, ciphertext) {
ok(!err);
ok(ciphertext, 'Encrypt item');
crypto_test.crypto.aesDecrypt(ciphertext, key, iv, function(decrypted) {
crypto_test.crypto.aesDecrypt(ciphertext, key, iv, function(err, decrypted) {
ok(!err);
equal(decrypted, secret, 'Decrypt item');
start();

View File

@ -12,6 +12,7 @@
<script>
// clear session storage of failed tests, so async order is correct after fail & refresh
window.sessionStorage.clear();
//window.Worker = undefined;
</script>
<!-- dependencies -->