diff --git a/.gitignore b/.gitignore index 7b35202b..90e9c209 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ resources/openpgpjs.pem build/ .DS_Store +node_modules +test/integration/lib \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..f5632394 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "0.10" +before_install: + - npm install -g grunt-cli \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..1909a371 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,52 @@ +module.exports = function(grunt) { + 'use strict'; + + // Project configuration. + grunt.initConfig({ + connect: { + dev: { + options: { + port: 8680, + base: '.', + keepalive: true + } + }, + test: { + options: { + port: 8681, + base: '.' + } + } + }, + + mocha: { + all: { + options: { + urls: ['http://localhost:<%= connect.test.options.port %>/test/integration/index.html'], + run: false, + reporter: 'Spec' + } + } + }, + + copy: { + npm: { + expand: true, + flatten: true, + cwd: 'node_modules/', + src: ['requirejs/require.js', 'mocha/mocha.css', 'mocha/mocha.js', 'chai/chai.js', 'sinon/pkg/sinon.js'], + dest: 'test/integration/lib/' + } + } + }); + + // Load the plugin(s) + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-connect'); + grunt.loadNpmTasks('grunt-mocha'); + + // Test/Dev tasks + grunt.registerTask('dev', ['connect:dev']); + grunt.registerTask('test', ['copy', 'connect:test', 'mocha']); + +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..38b4abf2 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "openpgpjs", + "version": "0.1.0-dev", + "engines": { + "node": ">=0.8" + }, + "scripts": { + "pretest": "make minify", + "test": "grunt test", + "start": "grunt dev" + }, + "dependencies": {}, + "devDependencies": { + "grunt": "0.4.1", + "mocha": "1.13.0", + "phantomjs": "1.9.1-9", + "requirejs": "2.1.8", + "chai": "1.7.2", + "sinon": "1.7.3", + "phantomjs": "1.9.1-9", + "grunt-contrib-connect": "0.5.0", + "grunt-contrib-copy": "0.4.1", + "grunt-mocha": "0.4.1" + } +} \ No newline at end of file diff --git a/test/integration/index.html b/test/integration/index.html new file mode 100644 index 00000000..ca87d20d --- /dev/null +++ b/test/integration/index.html @@ -0,0 +1,19 @@ + + + + + JavaScript Unit Tests + + + + +
+ + + + + + + + + diff --git a/test/integration/main.js b/test/integration/main.js new file mode 100644 index 00000000..f0703f35 --- /dev/null +++ b/test/integration/main.js @@ -0,0 +1,25 @@ +'use strict'; + +// config require.js +require.config({ + baseUrl: './', + paths: { + openpgp: '../../resources/openpgp.min' + }, + shim: { + openpgp: { + exports: 'window' + } + } +}); + +// start mocha tests +mocha.setup('bdd'); +require( + [ + 'pgp-test' + ], function() { + // require modules loaded -> run tests + mocha.run(); + } +); \ No newline at end of file diff --git a/test/integration/pgp-test.js b/test/integration/pgp-test.js new file mode 100644 index 00000000..1b115f2e --- /dev/null +++ b/test/integration/pgp-test.js @@ -0,0 +1,159 @@ +define(function(require) { + 'use strict'; + + var PGP = require('pgp'), + expect = chai.expect; + + describe('PGP Crypto Api unit tests', function() { + var pgp, + user = 'test@t-online.de', + passphrase = 'asdf', + keySize = 512, + keyId = 'F6F60E9B42CDFF4C', + pubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + + 'Version: OpenPGP.js v.1.20131011\n' + + 'Comment: http://openpgpjs.org\n' + + '\n' + + 'xk0EUlhMvAEB/2MZtCUOAYvyLFjDp3OBMGn3Ev8FwjzyPbIF0JUw+L7y2XR5\n' + + 'RVGvbK88unV3cU/1tOYdNsXI6pSp/Ztjyv7vbBUAEQEAAc0pV2hpdGVvdXQg\n' + + 'VXNlciA8d2hpdGVvdXQudGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhM\n' + + 'vQkQ9vYOm0LN/0wAAAW4Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXq\n' + + 'IiN602mWrkd8jcEzLsW5IUNzVPLhrFIuKyBDTpLnC07Loce1\n' + + '=6XMW\n' + + '-----END PGP PUBLIC KEY BLOCK-----', + privkey = '-----BEGIN PGP PRIVATE KEY BLOCK-----\n' + + 'Version: OpenPGP.js v.1.20131011\n' + + 'Comment: http://openpgpjs.org\n' + + '\n' + + 'xcBeBFJYTLwBAf9jGbQlDgGL8ixYw6dzgTBp9xL/BcI88j2yBdCVMPi+8tl0\n' + + 'eUVRr2yvPLp1d3FP9bTmHTbFyOqUqf2bY8r+72wVABEBAAH+AwMIhNB4ivtv\n' + + 'Y2xg6VeMcjjHxZayESHACV+nQx5Tx6ev6xzIF1Qh72fNPDppLhFSFOuTTMsU\n' + + 'kTN4c+BVYt29spH+cA1jcDAxQ2ULrNAXo+hheOqhpedTs8aCbcLFkJAS16hk\n' + + 'YSk4OnJgp/z24rVju1SHRSFbgundPzmNgXeX9e8IkviGhhQ11Wc5YwVkx03t\n' + + 'Z3MdDMF0jyhopbPIoBdyJB0dhvBh98w3JmwpYh9wjUA9MBHD1tvHpRmSZ3BM\n' + + 'UCmATn2ZLWBRWiYqFbgDnL1GM80pV2hpdGVvdXQgVXNlciA8d2hpdGVvdXQu\n' + + 'dGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhMvQkQ9vYOm0LN/0wAAAW4\n' + + 'Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXqIiN602mWrkd8jcEzLsW5\n' + + 'IUNzVPLhrFIuKyBDTpLnC07Loce1\n' + + '=ULta\n' + + '-----END PGP PRIVATE KEY BLOCK-----'; + + beforeEach(function() { + pgp = new PGP(); + }); + + afterEach(function() {}); + + describe('Generate key pair', function() { + it('should fail', function(done) { + pgp.generateKeys({ + emailAddress: 'test@t-onlinede', + keySize: keySize, + passphrase: passphrase + }, function(err, keys) { + expect(err).to.exist; + expect(keys).to.not.exist; + done(); + }); + }); + it('should fail', function(done) { + pgp.generateKeys({ + emailAddress: 'testt-online.de', + keySize: keySize, + passphrase: passphrase + }, function(err, keys) { + expect(err).to.exist; + expect(keys).to.not.exist; + done(); + }); + }); + it('should work', function(done) { + pgp.generateKeys({ + emailAddress: user, + keySize: keySize, + passphrase: passphrase + }, function(err, keys) { + expect(err).to.not.exist; + expect(keys.keyId).to.exist; + expect(keys.privateKeyArmored).to.exist; + expect(keys.publicKeyArmored).to.exist; + done(); + }); + }); + }); + + describe('Import/Export key pair', function() { + it('should fail', function(done) { + pgp.importKeys({ + passphrase: 'asd', + privateKeyArmored: privkey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.exist; + + pgp.exportKeys(function(err, keys) { + expect(err).to.exist; + expect(keys).to.not.exist; + done(); + }); + }); + }); + it('should work', function(done) { + pgp.importKeys({ + passphrase: passphrase, + privateKeyArmored: privkey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.not.exist; + + pgp.exportKeys(function(err, keys) { + expect(err).to.not.exist; + expect(keys.keyId).to.equal(keyId); + expect(keys.privateKeyArmored).to.equal(privkey); + expect(keys.publicKeyArmored).to.equal(pubkey); + done(); + }); + }); + }); + }); + + describe('Encryption', function() { + var message = 'Hello, World!', + ciphertext; + + beforeEach(function(done) { + pgp.importKeys({ + passphrase: passphrase, + privateKeyArmored: privkey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + + describe('Encrypt', function() { + it('should work', function(done) { + pgp.encrypt(message, [pubkey], function(err, ct) { + expect(err).to.not.exist; + expect(ct).to.exist; + ciphertext = ct; + done(); + }); + }); + }); + + describe('Decrypt', function() { + it('should work', function(done) { + pgp.decrypt(ciphertext, pubkey, function(err, pt) { + expect(err).to.not.exist; + expect(pt).to.equal(message); + done(); + }); + }); + }); + + }); + + }); +}); \ No newline at end of file diff --git a/test/integration/pgp.js b/test/integration/pgp.js new file mode 100644 index 00000000..8640e189 --- /dev/null +++ b/test/integration/pgp.js @@ -0,0 +1,173 @@ +/** + * High level crypto api that handles all calls to OpenPGP.js + */ +define(function(require) { + 'use strict'; + + var openpgp = require('openpgp').openpgp, + util = require('openpgp').util; + + var PGP = function() { + openpgp.init(); + }; + + /** + * Generate a key pair for the user + */ + PGP.prototype.generateKeys = function(options, callback) { + var keys, userId; + + if (!util.emailRegEx.test(options.emailAddress) || !options.keySize || typeof options.passphrase !== 'string') { + callback({ + errMsg: 'Crypto init failed. Not all options set!' + }); + return; + } + + // generate keypair (keytype 1=RSA) + try { + userId = 'Whiteout User <' + options.emailAddress + '>'; + keys = openpgp.generate_key_pair(1, options.keySize, userId, options.passphrase); + } catch (e) { + callback({ + errMsg: 'Keygeneration failed!', + err: e + }); + return; + } + + callback(null, { + keyId: util.hexstrdump(keys.privateKey.getKeyId()).toUpperCase(), + privateKeyArmored: keys.privateKeyArmored, + publicKeyArmored: keys.publicKeyArmored + }); + }; + + /** + * Import the user's key pair + */ + PGP.prototype.importKeys = function(options, callback) { + var publicKey, privateKey; + + // check passphrase + if (typeof options.passphrase !== 'string' || !options.privateKeyArmored || !options.publicKeyArmored) { + callback({ + errMsg: 'Importing keys failed. Not all options set!' + }); + return; + } + + // clear any keypair already in the keychain + openpgp.keyring.init(); + // unlock and import private key + if (!openpgp.keyring.importPrivateKey(options.privateKeyArmored, options.passphrase)) { + openpgp.keyring.init(); + callback({ + errMsg: 'Incorrect passphrase!' + }); + return; + } + // import public key + openpgp.keyring.importPublicKey(options.publicKeyArmored); + + // check if keys have the same id + privateKey = openpgp.keyring.exportPrivateKey(0); + publicKey = openpgp.keyring.getPublicKeysForKeyId(privateKey.keyId)[0]; + if (!privateKey || !privateKey.armored || !publicKey || !publicKey.armored || privateKey.keyId !== publicKey.keyId) { + // reset keyring + openpgp.keyring.init(); + callback({ + errMsg: 'Key IDs dont match!' + }); + return; + } + + callback(); + }; + + /** + * Export the user's key pair + */ + PGP.prototype.exportKeys = function(callback) { + var publicKey, privateKey; + + privateKey = openpgp.keyring.exportPrivateKey(0); + if (privateKey && privateKey.keyId) { + publicKey = openpgp.keyring.getPublicKeysForKeyId(privateKey.keyId)[0]; + } + + if (!privateKey || !privateKey.keyId || !privateKey.armored || !publicKey || !publicKey.armored) { + callback({ + errMsg: 'Could not export keys!' + }); + return; + } + + callback(null, { + keyId: util.hexstrdump(privateKey.keyId).toUpperCase(), + privateKeyArmored: privateKey.armored, + publicKeyArmored: publicKey.armored + }); + }; + + /** + * Encrypt and sign a pgp message for a list of receivers + */ + PGP.prototype.encrypt = function(plaintext, receiverKeys, callback) { + var ct, i, + privateKey = openpgp.keyring.exportPrivateKey(0).obj; + + for (i = 0; i < receiverKeys.length; i++) { + receiverKeys[i] = openpgp.read_publicKey(receiverKeys[i])[0]; + } + + ct = openpgp.write_signed_and_encrypted_message(privateKey, receiverKeys, plaintext); + + callback(null, ct); + }; + + /** + * Decrypt and verify a pgp message for a single sender + */ + PGP.prototype.decrypt = function(ciphertext, senderKey, callback) { + var privateKey = openpgp.keyring.exportPrivateKey(0).obj; + senderKey = openpgp.read_publicKey(senderKey)[0]; + + var msg = openpgp.read_message(ciphertext)[0]; + var keymat = null; + var sesskey = null; + + // Find the private (sub)key for the session key of the message + for (var i = 0; i < msg.sessionKeys.length; i++) { + if (privateKey.privateKeyPacket.publicKey.getKeyId() === msg.sessionKeys[i].keyId.bytes) { + keymat = { + key: privateKey, + keymaterial: privateKey.privateKeyPacket + }; + sesskey = msg.sessionKeys[i]; + break; + } + for (var j = 0; j < privateKey.subKeys.length; j++) { + if (privateKey.subKeys[j].publicKey.getKeyId() === msg.sessionKeys[i].keyId.bytes) { + keymat = { + key: privateKey, + keymaterial: privateKey.subKeys[j] + }; + sesskey = msg.sessionKeys[i]; + break; + } + } + } + if (keymat !== null) { + var decrypted = msg.decryptAndVerifySignature(keymat, sesskey, senderKey); + callback(null, decrypted.text); + + } else { + callback({ + errMsg: 'No private key found!' + }); + } + }; + + return PGP; +}); \ No newline at end of file