├── .gitignore ├── .npmignore ├── .shippable.yml ├── .travis.yml ├── Gruntfile.js ├── README.md ├── karma.conf.js ├── package.json ├── spec ├── e2e.spec.js └── integration │ └── pgpapi.spec.js └── src ├── demo ├── e2edemo.js ├── e2edemo.json ├── main.html └── main.js ├── e2e.js ├── googstorage.js ├── googstorage_mock.js ├── pgpapi.json ├── playground ├── README.md ├── index.html └── style.css └── test_mock.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | build/ 3 | dist/ 4 | node_modules/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .build/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.shippable.yml: -------------------------------------------------------------------------------- 1 | build_image: freedomjs/freedom 2 | language: node_js 3 | node_js: 4 | - 'stable' 5 | notifications: 6 | email: 7 | recipients: 8 | - freedom@cs.washington.edu 9 | on_success: change 10 | on_failure: always 11 | before_install: 12 | - "export DISPLAY=:10.0" 13 | - "Xvfb :10 -screen 0 1280x1024x24 &" 14 | install: 15 | - "google-chrome --version" 16 | - "firefox -v" 17 | - "shippable_retry npm install" 18 | script: 19 | - "grunt test --firefox-bin /usr/bin/firefox" 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 'stable' 5 | before_install: 6 | - npm install -g grunt-cli 7 | cache: 8 | directories: 9 | - node_modules 10 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gruntfile for freedom-pgp-e2e 3 | * 4 | * This repository uses JavaScript crypto code from 5 | * Google's end-to-end project to provide a pgp-like 6 | * freedom crypto API. Note that (for now) the build 7 | * process is a bit unorthodox (clones e2e repo from 8 | * Google Code - see README.md). 9 | **/ 10 | 11 | module.exports = function(grunt) { 12 | require('time-grunt')(grunt); 13 | require('jit-grunt')(grunt, { 14 | 'npm-publish': 'grunt-npm' 15 | }); 16 | 17 | grunt.initConfig({ 18 | copy: { 19 | build: { 20 | src: ['src/*.js*'], 21 | dest: 'build/', 22 | flatten: true, 23 | filter: 'isFile', 24 | expand: true 25 | }, 26 | freedom: { 27 | src: [require.resolve('freedom')], 28 | dest: 'build/', 29 | flatten: true, 30 | filter: 'isFile', 31 | expand: true 32 | }, 33 | demo: { 34 | src: ['src/demo/*'], 35 | dest: 'build/demo/', 36 | flatten: true, 37 | filter: 'isFile', 38 | expand: true 39 | }, 40 | playground: { 41 | src: ['src/playground/*'], 42 | dest: 'build/playground/', 43 | flatten: true, 44 | filter: 'isFile', 45 | expand: true 46 | }, 47 | e2eCompiledJavaScript: { 48 | src: [require.resolve('e2e')], 49 | dest: 'build/end-to-end.compiled.js', 50 | onlyIf: 'modified' 51 | }, 52 | dist: { 53 | src: ['build/*'], 54 | dest: 'dist/', 55 | flatten: true, 56 | filter: 'isFile', 57 | expand: true 58 | } 59 | }, 60 | 61 | browserify: { 62 | hex2words: { 63 | files: { 64 | 'build/hex2words.js': [require.resolve('hex2words')] 65 | }, 66 | options: { 67 | browserifyOptions: { 68 | standalone: 'hex2words' 69 | } 70 | } 71 | } 72 | }, 73 | 74 | karma: { 75 | options: { 76 | configFile: 'karma.conf.js' 77 | }, 78 | browsers: { 79 | singleRun: true, 80 | autoWatch: false 81 | }, 82 | watch: { 83 | singleRun: false, 84 | autoWatch: true, 85 | reporters: ['progress', 'story'], 86 | preprocessors: {}, 87 | coverageReporter: {} 88 | }, 89 | phantom: { 90 | browsers: ['PhantomJS'], 91 | singleRun: true, 92 | autoWatch: false 93 | } 94 | }, 95 | 96 | jasmine_chromeapp: { 97 | src: ['node_modules/freedom-for-chrome/freedom-for-chrome.*', 98 | 'spec/integration/pgpapi.spec.js', 'build/*.js*', 'build/demo/*'], 99 | options: { 100 | paths: ['node_modules/freedom-for-chrome/freedom-for-chrome.js', 101 | 'spec/integration/pgpapi.spec.js'], 102 | keepRunner: false 103 | } 104 | }, 105 | 106 | jasmine_firefoxaddon: { 107 | tests: ['spec/integration/pgpapi.spec.js'], 108 | resources: ['build/*js*', 'build/demo/*'], 109 | helpers: ['node_modules/freedom-for-firefox/freedom-for-firefox.jsm'] 110 | }, 111 | 112 | jasmine_nodejs: { 113 | integration: { specs: ['spec/integration/**']} 114 | }, 115 | 116 | jshint: { 117 | all: ['src/**/*.js', 'spec/**/*.js'], 118 | options: { 119 | jshintrc: true, 120 | reporter: require('jshint-stylish') 121 | } 122 | }, 123 | 124 | connect: { 125 | demo: { 126 | options: { 127 | port: 8000, 128 | keepalive: true, 129 | base: ['./', 'build/'], 130 | open: 'http://localhost:8000/build/demo/main.html' 131 | } 132 | } 133 | }, 134 | 135 | bump: { 136 | options: { 137 | files: ['package.json'], 138 | commit: true, 139 | commitMessage: 'Release v%VERSION%', 140 | commitFiles: ['package.json'], 141 | createTag: true, 142 | tagName: 'v%VERSION%', 143 | tagMessage: 'Version %VERSION%', 144 | push: true, 145 | pushTo: 'origin' 146 | } 147 | }, 148 | 149 | 'npm-publish': { 150 | options: { 151 | // list of tasks that are required before publishing 152 | requires: [], 153 | // if the workspace is dirty, abort publishing (to avoid publishing local changes) 154 | abortIfDirty: true 155 | } 156 | }, 157 | 158 | prompt: { 159 | tagMessage: { 160 | options: { 161 | questions: [ 162 | { 163 | config: 'bump.options.tagMessage', 164 | type: 'input', 165 | message: 'Enter a git tag message:', 166 | default: 'v%VERSION%' 167 | } 168 | ] 169 | } 170 | } 171 | }, 172 | 173 | clean: ['build/', '.build/', 'dist/'] 174 | }); 175 | 176 | grunt.registerTask('build', [ 177 | 'jshint', 178 | 'copy:build', 179 | 'copy:freedom', 180 | 'copy:demo', 181 | 'copy:playground', 182 | 'copy:e2eCompiledJavaScript', 183 | 'browserify:hex2words', 184 | 'copy:dist' 185 | ]); 186 | grunt.registerTask('test', [ 187 | 'build', 188 | 'karma:browsers', 189 | 'karma:phantom', 190 | 'jasmine_chromeapp', 191 | 'jasmine_nodejs', 192 | 'jasmine_firefoxaddon' 193 | ]); 194 | grunt.registerTask('ci', [ 195 | 'build', 196 | 'jasmine_nodejs' 197 | ]); 198 | grunt.registerTask('release', function(arg) { 199 | if (arguments.length === 0) { 200 | arg = 'patch'; 201 | } 202 | grunt.task.run([ 203 | 'test', 204 | 'prompt:tagMessage', 205 | 'bump:' + arg, 206 | 'npm-publish' 207 | ]); 208 | }); 209 | grunt.registerTask('demo', [ 210 | 'build', 211 | 'connect' 212 | ]); 213 | grunt.registerTask('default', [ 214 | 'build', 215 | 'karma:phantom' 216 | ]); 217 | 218 | } 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | freedom-pgp-e2e 2 | ================= 3 | [![Build Status](https://travis-ci.org/freedomjs/freedom-pgp-e2e.svg?branch=master)](https://travis-ci.org/freedomjs/freedom-pgp-e2e)[![Build Status](https://api.shippable.com/projects/54c823bf5ab6cc135289fbec/badge?branchName=master)](https://app.shippable.com/projects/54c823bf5ab6cc135289fbec/builds/latest) 4 | 5 | Wrapping up [end-to-end code](https://github.com/google/end-to-end) 6 | and providing PGP crypto functionality in a freedom custom API. 7 | 8 | #### Build 9 | The normal flow is just npm install, and then the 10 | following grunt options: 11 | 12 | - grunt (default, will lint, copy, and run phantom tests) 13 | 14 | - grunt test (will lint, copy, and run all tests) 15 | 16 | - grunt demo (will lint, copy, and open a browser pointed at the demo 17 | app) 18 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Sep 23 2014 15:17:47 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | // to avoid DISCONNECTED messages 11 | browserDisconnectTimeout : 10000, // default 2000 12 | browserDisconnectTolerance : 1, // default 0 13 | browserNoActivityTimeout : 60000, // default 10000 14 | 15 | // frameworks to use 16 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 17 | frameworks: ['jasmine'], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | 'node_modules/es6-promise/dist/es6-promise.min.js', // es6 promises 22 | 'build/end-to-end.compiled.js', // to coerce loading first 23 | 'build/*.js', 24 | 'spec/*.js' 25 | ], 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | 'build/freedom.js', 30 | 'build/googstorage.js' 31 | ], 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | }, 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 41 | reporters: ['progress'], 42 | 43 | // web server port 44 | port: 9876, 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | // level of logging 50 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 51 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 52 | logLevel: config.LOG_INFO, 53 | 54 | // enable / disable watching file and executing tests whenever any file changes 55 | autoWatch: false, 56 | 57 | // start these browsers 58 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 59 | browsers: ['Chrome', 'Firefox'], 60 | 61 | // Continuous Integration mode 62 | // if true, Karma captures browsers, runs the tests and exits 63 | singleRun: false 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freedom-pgp-e2e", 3 | "description": "Library to provide freedom API wrapper of end-to-end library", 4 | "version": "0.6.5", 5 | "contributors": [ 6 | "Aaron Gallant " 7 | ], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/freedomjs/freedom-pgp-e2e" 11 | }, 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "e2e": "^0.0.8", 15 | "es6-promise": "^3.1.2", 16 | "freedom": "^0.6.30", 17 | "freedom-for-chrome": "^0.4.24", 18 | "freedom-for-firefox": "^0.6.23", 19 | "freedom-for-node": "^0.2.19", 20 | "grunt": "^0.4.5", 21 | "grunt-browserify": "^5.0.0", 22 | "grunt-bump": "^0.7.3", 23 | "grunt-contrib-clean": "^1.0.0", 24 | "grunt-contrib-connect": "^1.0.2", 25 | "grunt-contrib-copy": "^1.0.0", 26 | "grunt-contrib-jshint": "^1.0.0", 27 | "grunt-jasmine-chromeapp": "^1.10.0", 28 | "grunt-jasmine-firefoxaddon": "^0.3.6", 29 | "grunt-jasmine-nodejs": "^1.5.2", 30 | "grunt-karma": "^1.0.0", 31 | "grunt-npm": "^0.0.2", 32 | "grunt-prompt": "^1.3.3", 33 | "hex2words": "^1.1.3", 34 | "jit-grunt": "^0.10.0", 35 | "jshint-stylish": "^2.2.0", 36 | "json-store": "0.0.1", 37 | "karma": "^0.13.22", 38 | "karma-chrome-launcher": "^1.0.1", 39 | "karma-coverage": "^1.0.0", 40 | "karma-firefox-launcher": "^1.0.0", 41 | "karma-jasmine": "^1.0.2", 42 | "karma-phantomjs-launcher": "^1.0.0", 43 | "karma-story-reporter": "^0.3.1", 44 | "karma-unicorn-reporter": "^0.1.4", 45 | "phantomjs-prebuilt": "^2.1.7", 46 | "time-grunt": "^1.3.0" 47 | }, 48 | "scripts": { 49 | "test": "grunt ci --verbose" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /spec/e2e.spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, require, expect, it*/ 2 | /*jslint indent:2*/ 3 | 4 | // Unit tests for e2e directly 5 | // Tests both freedom API calls and internal methods 6 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; 7 | describe('e2eImp', function () { 8 | var publicKeyStr = 9 | '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n' + 10 | 'Charset: UTF-8\r\n' + 11 | '\r\n' + 12 | 'xv8AAABSBFPIW9ETCCqGSM49AwEHAgMEh9yJj8tEYplKXKKiTWphXYkJEQSbm0GH\r\n' + 13 | 'hy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3TqVWCNdRpwgcKs3/AAAAFjxx\r\n' + 14 | 'dWFudHN3b3JkQGdtYWlsLmNvbT7C/wAAAGYEEBMIABj/AAAABYJTyFvR/wAAAAmQ\r\n' + 15 | '6bggH1uHbYkAAPefAQDgx/omfDRc7hB4DT1Eong2ytygVXMIuQJmRjnKxqM61AEA\r\n' + 16 | 'g5D6nKw1Woicmg7x2qfj7wU+eLlZ5UXTNqjpe8xQ4+3O/wAAAFYEU8hb0RIIKoZI\r\n' + 17 | 'zj0DAQcCAwS10YFtrIWwvvLE8r32gCEtDD7Cnefkem6Tz4fDFlrdrAUNXADxGLaq\r\n' + 18 | 'AQsgmceluPWjIBY7GtMvd6z/biN8YOANAwEIB8L/AAAAZgQYEwgAGP8AAAAFglPI\r\n' + 19 | 'W9H/AAAACZDpuCAfW4dtiQAAegAA/RYXPbjEOHc7iy3xFxWKWPvpnPc5LwX/6DDt\r\n' + 20 | 'woPMCTLeAQCpjnRiMaIK7tjslDfXd4BtaY6K90JHuRPCQUJ7Uw+fRA==\r\n' + 21 | '=3Iv4\r\n' + 22 | '-----END PGP PUBLIC KEY BLOCK-----\r\n'; 23 | 24 | var keyFingerprint = 'B734 A06E 3413 DD98 6774 3FB3 E9B8 201F 5B87 6D89'; 25 | var keyWords = ["seabird", "confidence", "ragtime", "headwaters", 26 | "choking", "barbecue", "swelter", "narrative", 27 | "freedom", "hydraulic", "cowbell", "pocketful", 28 | "treadmill", "provincial", "bison", "businessman", 29 | "erase", "liberty", "goggles", "matchmaker"]; 30 | 31 | var privateKeyStr = 32 | '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n' + 33 | 'Charset: UTF-8\r\n' + 34 | 'Version: End-To-End v0.3.1338\r\n' + 35 | '\r\n' + 36 | 'xf8AAAB3BFPIW9ETCCqGSM49AwEHAgMEh9yJj8tEYplKXKKiTWphXYkJEQSbm0GH\r\n' + 37 | 'hy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3TqVWCNdRpwgcKgABAIaxz+cn\r\n' + 38 | 'aR1CNIhNGoo7m0T8RycWCslolvmV6JnSFzhYDn3N/wAAABY8cXVhbnRzd29yZEBn\r\n' + 39 | 'bWFpbC5jb20+wv8AAABmBBATCAAY/wAAAAWCU8hb0f8AAAAJkOm4IB9bh22JAAD3\r\n' + 40 | 'nwEA4Mf6Jnw0XO4QeA09RKJ4NsrcoFVzCLkCZkY5ysajOtQBAIOQ+pysNVqInJoO\r\n' + 41 | '8dqn4+8FPni5WeVF0zao6XvMUOPtx/8AAAB7BFPIW9ESCCqGSM49AwEHAgMEtdGB\r\n' + 42 | 'bayFsL7yxPK99oAhLQw+wp3n5Hpuk8+HwxZa3awFDVwA8Ri2qgELIJnHpbj1oyAW\r\n' + 43 | 'OxrTL3es/24jfGDgDQMBCAcAAP40eoOaXxwE/EIXZOddFf+423N12TuuQfqPREhx\r\n' + 44 | 'KOMOAg94wv8AAABmBBgTCAAY/wAAAAWCU8hb0f8AAAAJkOm4IB9bh22JAAB6AAD/\r\n' + 45 | 'R8thL3J2WQsIviAWAZFaip8WCzom60sXCfb3eVC3Eg4BAMR+IehbobVWr3AEdNIj\r\n' + 46 | 'MjSM+cgdhFBqQqQyxFOaX3kRxv8AAABSBFPIW9ETCCqGSM49AwEHAgMEh9yJj8tE\r\n' + 47 | 'YplKXKKiTWphXYkJEQSbm0GHhy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3\r\n' + 48 | 'TqVWCNdRpwgcKs3/AAAAFjxxdWFudHN3b3JkQGdtYWlsLmNvbT7C/wAAAGYEEBMI\r\n' + 49 | 'ABj/AAAABYJTyFvR/wAAAAmQ6bggH1uHbYkAAPefAQDgx/omfDRc7hB4DT1Eong2\r\n' + 50 | 'ytygVXMIuQJmRjnKxqM61AEAg5D6nKw1Woicmg7x2qfj7wU+eLlZ5UXTNqjpe8xQ\r\n' + 51 | '4+3O/wAAAFYEU8hb0RIIKoZIzj0DAQcCAwS10YFtrIWwvvLE8r32gCEtDD7Cnefk\r\n' + 52 | 'em6Tz4fDFlrdrAUNXADxGLaqAQsgmceluPWjIBY7GtMvd6z/biN8YOANAwEIB8L/\r\n' + 53 | 'AAAAZgQYEwgAGP8AAAAFglPIW9H/AAAACZDpuCAfW4dtiQAAegAA/RYXPbjEOHc7\r\n' + 54 | 'iy3xFxWKWPvpnPc5LwX/6DDtwoPMCTLeAQCpjnRiMaIK7tjslDfXd4BtaY6K90JH\r\n' + 55 | 'uRPCQUJ7Uw+fRA==\r\n' + 56 | '=H/6h\r\n' + 57 | '-----END PGP PRIVATE KEY BLOCK-----\r\n'; 58 | 59 | var secondPrivKeyStr = 60 | '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n' + 61 | 'Charset: UTF-8\r\n' + 62 | 'Version: End-To-End v0.31337.1\r\n' + 63 | '\r\n' + 64 | 'xf8AAAB3BAAAAAATCCqGSM49AwEHAgMEFBwMuJYcFZ8diyWqImSpcjIDVXvrvTqi\r\n' + 65 | '3ZW6aP2Xa7hM1RzpXNEJWf0MmSXcYPDF10t1NYGyFObi9MRGoVI1uAABAIpkPlZ0\r\n' + 66 | 'ekouOO8LrzBQyBbeR8/E+BrC5/SVweBFSHeqEHPN/wAAABQ8dXNlci0xQGV4YW1w\r\n' + 67 | 'bGUuY29tPsL/AAAAjgQQEwgAQP8AAAAFglartIL/AAAAAosJ/wAAAAmQXj/bCxIp\r\n' + 68 | 'OcD/AAAABZUICQoL/wAAAASWAwEC/wAAAAKbA/8AAAACngEAALZtAP40gWxAFmWi\r\n' + 69 | 'QtX0NcwueplZ9NGDXRmiZ/nib0YUDLISUgD/QlSj7uQMeakFNjXxRz8V1BqzyMRC\r\n' + 70 | 'kxzgWJz+n5S8KuPH/wAAAHsEAAAAABIIKoZIzj0DAQcCAwS1SHoSCjjOKdpzbUN/\r\n' + 71 | 'eIFRG9YdMaUUzFmDQ7UhbKZP/MNcBdACy87jVwI2tNh5rSWLhqPiEegMqTXZsqxd\r\n' + 72 | '2ki3AwEIBwAA/1iPkMsIvyzP2idPN2y+wFY/zmDG2xR8nzVNEcXnZCR9EEvC/wAA\r\n' + 73 | 'AG0EGBMIAB//AAAABYJWq7SC/wAAAAmQXj/bCxIpOcD/AAAAApsMAADG6AD/RDyt\r\n' + 74 | 'sJ4dQ7FNACg5kvuC8y4MhO6EsoStVZSHgFXx6z8A/3y62Mh4rdBtjstmZase21OV\r\n' + 75 | 'Bzyi7hW5y4rRJHRSg1bJxv8AAABSBAAAAAATCCqGSM49AwEHAgMEFBwMuJYcFZ8d\r\n' + 76 | 'iyWqImSpcjIDVXvrvTqi3ZW6aP2Xa7hM1RzpXNEJWf0MmSXcYPDF10t1NYGyFObi\r\n' + 77 | '9MRGoVI1uM3/AAAAFDx1c2VyLTFAZXhhbXBsZS5jb20+wv8AAACOBBATCABA/wAA\r\n' + 78 | 'AAWCVqu0gv8AAAACiwn/AAAACZBeP9sLEik5wP8AAAAFlQgJCgv/AAAABJYDAQL/\r\n' + 79 | 'AAAAApsD/wAAAAKeAQAAtm0A/jSBbEAWZaJC1fQ1zC56mVn00YNdGaJn+eJvRhQM\r\n' + 80 | 'shJSAP9CVKPu5Ax5qQU2NfFHPxXUGrPIxEKTHOBYnP6flLwq487/AAAAVgQAAAAA\r\n' + 81 | 'EggqhkjOPQMBBwIDBLVIehIKOM4p2nNtQ394gVEb1h0xpRTMWYNDtSFspk/8w1wF\r\n' + 82 | '0ALLzuNXAja02HmtJYuGo+IR6AypNdmyrF3aSLcDAQgHwv8AAABtBBgTCAAf/wAA\r\n' + 83 | 'AAWCVqu0gv8AAAAJkF4/2wsSKTnA/wAAAAKbDAAAxugBAMXSmje/LKMk1EyIrn4+\r\n' + 84 | 'xcwKRzUiuDeg1EKsUWGcIVwcAPoCJlH/uaVStEdVDaLRqgvhYzUJOMWZgBz1Vn7I\r\n' + 85 | 'JmFLrg==\r\n' + 86 | '=UvKo\r\n' + 87 | '-----END PGP PRIVATE KEY BLOCK-----\r\n'; 88 | 89 | var e2eImp; 90 | var buffer = new ArrayBuffer(12); 91 | var byteView = new Uint8Array(buffer); 92 | // bytes for the string "abcd1234" 93 | byteView.set([49, 50, 51, 52, 49, 50, 51, 52, 49, 50, 51, 52]); 94 | var sharedSecret = [216,221,208,16,30,17,41,250,204,28,94,208,188,132,206,121, 95 | 155,132,218,70,135,211,34,169,49,149,244,96,43,111,12,224]; 96 | 97 | beforeEach(function () { 98 | e2eImp = new mye2e(); 99 | }); 100 | 101 | it('reject invalid userid', function(done) { 102 | e2eImp.setup('test passphrase', 'bad user@id').then( 103 | function() { 104 | console.log(e2eImp); // shouldn't see this, should go to error case 105 | expect(false).toBeTruthy(); 106 | }).catch(function(e) { 107 | expect(e).toEqual('Invalid userid, expected: "name "'); 108 | }).then(done); 109 | }); 110 | 111 | it('test importKey and deleteKey with public key', function(done) { 112 | e2eImp.setup('test passphrase', 'Test User ').then( 113 | function () { 114 | return e2eImp.importPubKey(publicKeyStr); 115 | }).then(function (keyObj) { 116 | expect(keyObj.key.fingerprintHex).toEqual(keyFingerprint); 117 | return e2eImp.searchPublicKey(''); 118 | }).then(function (keys) { 119 | expect(keys.length).toEqual(1); 120 | expect(keys[0].uids[0]).toEqual(''); 121 | }).then(function () { 122 | return e2eImp.deleteKey(''); 123 | }).then(function () { 124 | return e2eImp.searchPublicKey(''); 125 | }).then(function (keys) { 126 | expect(keys.length).toEqual(0); 127 | }).catch(function (e) { 128 | console.log(e.toString()); 129 | expect(false).toBeTruthy(); 130 | }).then(done); 131 | }); 132 | 133 | it('test getFingerprint with public key', function(done) { 134 | e2eImp.setup('test passphrase', 'Test User ').then( 135 | function () { 136 | return e2eImp.getFingerprint(publicKeyStr); 137 | }).then(function (result) { 138 | expect(result.fingerprint).toEqual(keyFingerprint); 139 | expect(result.words).toEqual(keyWords); 140 | }).catch(function (e) { 141 | console.log(e.toString()); 142 | expect(false).toBeTruthy(); 143 | }).then(done); 144 | }); 145 | 146 | it('test importKey and deleteKey with private key', function(done) { 147 | e2eImp.setup('test passphrase', 'Test User ').then( 148 | function () { 149 | return e2eImp.importPrivKey(privateKeyStr); 150 | }).then(function () { 151 | return e2eImp.searchPrivateKey(''); 152 | }).then(function (keys) { 153 | expect(keys.length).toEqual(1); 154 | expect(keys[0].uids[0]).toEqual(''); 155 | }).then(function () { 156 | return e2eImp.deleteKey(''); 157 | }).then(function () { 158 | return e2eImp.searchPrivateKey(''); 159 | }).then(function (keys) { 160 | expect(keys.length).toEqual(0); 161 | }).catch(function (e) { 162 | console.log(e.toString()); 163 | expect(false).toBeTruthy(); 164 | }).then(done); 165 | }); 166 | 167 | it('test importKeypair', function(done) { 168 | e2eImp.importKeypair('', '', privateKeyStr) 169 | .then(function () { 170 | expect(e2eImp.pgpUser).toEqual(''); 171 | return e2eImp.exportKey(); 172 | }).then(function (publicKey) { 173 | expect(publicKey.key).toEqual(publicKeyStr); 174 | expect(publicKey.fingerprint).toEqual(keyFingerprint); 175 | expect(publicKey.words).toEqual(keyWords); 176 | }).then(function () { 177 | return e2eImp.searchPrivateKey(''); 178 | }).then(function (keys) { 179 | expect(keys.length).toEqual(1); 180 | expect(keys[0].uids[0]).toEqual(''); 181 | }).catch(function (e) { 182 | console.log(e.toString()); 183 | expect(false).toBeTruthy(); 184 | }).then(done); 185 | }); 186 | 187 | it('encrypt and decrypt', function(done) { 188 | e2eImp.setup('test passphrase', 'Test User ').then( 189 | function () { 190 | return e2eImp.exportKey(); 191 | }).then(function (publicKey) { 192 | return e2eImp.signEncrypt(buffer, publicKey.key, false); 193 | }).then(function (encryptedData) { 194 | return e2eImp.verifyDecrypt(encryptedData); 195 | }).then(function (result) { 196 | expect(result.data).toEqual(buffer); 197 | }).catch(function (e) { 198 | console.log(e.toString()); 199 | expect(false).toBeTruthy(); 200 | }).then(done); 201 | }); 202 | 203 | it('sign and verify', function(done) { 204 | e2eImp.setup('test passphrase', 'Test User ').then( 205 | function () { 206 | return Promise.all([e2eImp.exportKey(), e2eImp.signEncrypt(buffer)]); 207 | }).then(function (array) { 208 | var key = array[0].key; 209 | var signedData = array[1]; 210 | return e2eImp.verifyDecrypt(signedData, key); 211 | }).then(function (result) { 212 | expect(result.data).toEqual(buffer); 213 | }).catch(function (e) { 214 | console.log(e.toString()); 215 | expect(false).toBeTruthy(); 216 | }).then(done); 217 | }); 218 | 219 | it('encryptSign and verifyDecrypt', function(done) { 220 | e2eImp.setup('test passphrase', 'Test User ').then( 221 | function () { 222 | return e2eImp.exportKey(); 223 | }).then(function (publicKey) { 224 | return e2eImp.signEncrypt(buffer, publicKey.key, true).then( 225 | function (encryptedData) { 226 | return e2eImp.verifyDecrypt(encryptedData, publicKey.key); 227 | }); 228 | }).then(function (result) { 229 | expect(result.data).toEqual(buffer); 230 | expect(result.signedBy.length).toEqual(1); 231 | expect(result.signedBy[0]).toEqual('Test User '); 232 | }).catch(function (e) { 233 | console.log(e.toString()); 234 | expect(false).toBeTruthy(); 235 | }).then(done); 236 | }); 237 | 238 | it('generate keys', function(done) { 239 | e2eImp.setup('test passphrase', 'Test User ').then( 240 | function () { 241 | expect(true).toBeTruthy(); 242 | return e2eImp.searchPrivateKey('Test User '); 243 | }).then(function (keys) { 244 | expect(keys.length).toEqual(1); 245 | expect(keys[0].uids[0]).toEqual('Test User '); 246 | }).catch(function (e) { 247 | console.log(e.toString()); 248 | expect(false).toBeTruthy(); 249 | }).then(done); 250 | }); 251 | 252 | it('export public key', function(done) { 253 | e2eImp.setup('test passphrase', 'Test User ').then( 254 | function () { 255 | expect(true).toBeTruthy(); 256 | return e2eImp.exportKey(); 257 | }).then(function (publicKey) { 258 | expect(publicKey.key.length > 36); 259 | expect(publicKey.key.substring(0, 36)).toEqual( 260 | '-----BEGIN PGP PUBLIC KEY BLOCK-----'); 261 | expect(publicKey.key.substring( 262 | publicKey.key.length - 36, publicKey.key.length)).toEqual( 263 | '-----END PGP PUBLIC KEY BLOCK-----\r\n'); 264 | }).catch(function (e) { 265 | console.log(e.toString()); 266 | expect(false).toBeTruthy(); 267 | }).then(done); 268 | }); 269 | 270 | it('export public key *without* PGP headers', function(done) { 271 | e2eImp.setup('test passphrase', 'Test User ').then( 272 | function () { 273 | expect(true).toBeTruthy(); 274 | return e2eImp.exportKey(true); 275 | }).then(function (publicKey) { 276 | expect(publicKey.key.length > 36); 277 | expect(publicKey.key.substring(0, 36)).not.toEqual( 278 | '-----BEGIN PGP PUBLIC KEY BLOCK-----'); 279 | expect(publicKey.key.substring( 280 | publicKey.key.length - 36, publicKey.key.length)).not.toEqual( 281 | '-----END PGP PUBLIC KEY BLOCK-----\r\n'); 282 | }).catch(function (e) { 283 | console.log(e.toString()); 284 | expect(false).toBeTruthy(); 285 | }).then(done); 286 | }); 287 | 288 | it('armor and dearmor', function(done) { 289 | e2eImp.setup('test passphrase', 'Test User ').then( 290 | function () { 291 | return e2eImp.armor(buffer); 292 | }).then(function (armored) { 293 | return e2eImp.dearmor(armored); 294 | }).then(function (dearmored) { 295 | expect(dearmored).toEqual(buffer); 296 | }).catch(function (e) { 297 | console.log(e.toString()); 298 | expect(false).toBeTruthy(); 299 | }).then(done); 300 | }); 301 | 302 | it('sign and verify between two users', function(done) { 303 | e2eImp.setup('test passphrase', 'Test User ').then(function() { 304 | var otherUser = new mye2e(); 305 | return otherUser.setup('other user passphrase', 306 | 'Other User ').then(function() { 307 | return otherUser; 308 | }); 309 | }).then( 310 | function(otherUser) { 311 | return Promise.all([otherUser.exportKey(), 312 | otherUser.signEncrypt(buffer)]); 313 | }).then(function (array) { 314 | var key = array[0].key; 315 | var signedData = array[1]; 316 | return e2eImp.verifyDecrypt(signedData, key); 317 | }).then(function (result) { 318 | expect(result.data).toEqual(buffer); 319 | }).catch(function (e) { 320 | console.log(e.toString()); 321 | expect(false).toBeTruthy(); 322 | }).then(done); 323 | }); 324 | 325 | it('getFingerprint of users with the same name', function(done) { 326 | var otherUser0 = new mye2e(); 327 | var otherUser1 = new mye2e(); 328 | e2eImp.setup('test passphrase', 'Test User ').then( 329 | function () { 330 | return Promise.all([ 331 | otherUser0.setup('test passphrase', 'Test User '), 332 | otherUser1.setup('test passphrase', 'Test User ') 333 | ]); 334 | }).then(function () { 335 | return Promise.all([otherUser0.exportKey(), otherUser1.exportKey()]); 336 | }).then(function (keys) { 337 | // Check that the test is functioning correctly, generating new keys. 338 | expect(keys[0].key).not.toEqual(keys[1].key); 339 | expect(keys[0].fingerprint).not.toEqual(keys[1].fingerprint); 340 | 341 | // Check that the fingerprints are computed correctly 342 | return Promise.all([ 343 | e2eImp.getFingerprint(keys[0].key).then(function(fp) { 344 | expect(fp.fingerprint).toEqual(keys[0].fingerprint); 345 | }), 346 | e2eImp.getFingerprint(keys[1].key).then(function(fp) { 347 | expect(fp.fingerprint).toEqual(keys[1].fingerprint); 348 | }) 349 | ]); 350 | }).catch(function (e) { 351 | console.log(e.toString()); 352 | expect(false).toBeTruthy(); 353 | }).then(done); 354 | }); 355 | 356 | it('verify messages from users with the same name', function(done) { 357 | var otherUser0 = new mye2e(); 358 | var otherUser1 = new mye2e(); 359 | e2eImp.setup('test passphrase', 'Test User ').then(function() { 360 | return Promise.all([ 361 | otherUser0.setup('test passphrase', 'Test User '), 362 | otherUser1.setup('test passphrase', 'Test User ') 363 | ]); 364 | }).then(function () { 365 | // Sign a message from user0, and verify with e2eImp 366 | return Promise.all([otherUser0.exportKey(), 367 | otherUser0.signEncrypt(buffer)]); 368 | }).then(function (array) { 369 | var key = array[0].key; 370 | var signedData = array[1]; 371 | return e2eImp.verifyDecrypt(signedData, key); 372 | }).then(function (result) { 373 | expect(result.data).toEqual(buffer); 374 | 375 | // Sign a message from user1, and verify with e2eImp 376 | return Promise.all([otherUser1.exportKey(), 377 | otherUser1.signEncrypt(buffer)]); 378 | }).then(function (array) { 379 | var key = array[0].key; 380 | var signedData = array[1]; 381 | return e2eImp.verifyDecrypt(signedData, key); 382 | }).then(function (result) { 383 | expect(result.data).toEqual(buffer); 384 | }).catch(function (e) { 385 | console.log(e.toString()); 386 | expect(false).toBeTruthy(); 387 | }).then(done); 388 | }); 389 | 390 | it('encrypt messages to users with the same name', function(done) { 391 | var otherUser0 = new mye2e(); 392 | var otherUser1 = new mye2e(); 393 | e2eImp.setup('test passphrase', 'Test User ').then(function() { 394 | return Promise.all([ 395 | otherUser0.setup('test passphrase', 'Test User '), 396 | otherUser1.setup('test passphrase', 'Test User ') 397 | ]); 398 | }).then(function () { 399 | return Promise.all([otherUser0.exportKey(), otherUser1.exportKey()]); 400 | }).then(function (keys) { 401 | // Check that the test is functioning correctly, generating new keys. 402 | expect(keys[0].key).not.toEqual(keys[1].key); 403 | 404 | // Encrypt a message to both users 405 | return Promise.all([ 406 | e2eImp.signEncrypt(buffer, keys[0].key), 407 | e2eImp.signEncrypt(buffer, keys[1].key) 408 | ]); 409 | }).then(function (cipherTexts) { 410 | return Promise.all([ 411 | otherUser0.verifyDecrypt(cipherTexts[0]), 412 | otherUser1.verifyDecrypt(cipherTexts[1]) 413 | ]); 414 | }).then(function (results) { 415 | expect(results[0].data).toEqual(buffer); 416 | expect(results[1].data).toEqual(buffer); 417 | }).catch(function (e) { 418 | console.log(e.toString()); 419 | expect(false).toBeTruthy(); 420 | }).then(done); 421 | }); 422 | 423 | it('generates a shared secret', function(done) { 424 | function array2str(buffer) { 425 | var a = new DataView(buffer); 426 | var str = ''; 427 | for (var i = 0; i < a.byteLength; i++) { 428 | if (i > 0) { 429 | str += ","; 430 | } 431 | str += a.getUint8(i).toString(); 432 | } 433 | return str; 434 | } 435 | 436 | function compareBufferToArray(buffer, array) { 437 | var buf = new DataView(buffer); 438 | if (buf.byteLength != array.length) return false; 439 | for (var i = 0; i < buf.byteLength; i++) { 440 | if (buf.getUint8(i) != array[i]) return false; 441 | } 442 | return true; 443 | } 444 | 445 | e2eImp.importKeypair('', '', secondPrivKeyStr) 446 | .then(function() { 447 | return e2eImp.ecdhBob('P_256', publicKeyStr); 448 | }).then(function(secret) { 449 | expect(compareBufferToArray(secret, sharedSecret)).toBeTruthy(); 450 | }).catch(function (e) { 451 | console.log(e.toString()); 452 | expect(false).toBeTruthy(); 453 | }).then(done); 454 | }); 455 | }); 456 | -------------------------------------------------------------------------------- /spec/integration/pgpapi.spec.js: -------------------------------------------------------------------------------- 1 | /*globals describe, beforeEach, require, expect, it*/ 2 | /*jslint indent:2,node:true*/ 3 | 4 | var publicKeyStr = 5 | '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n' + 6 | 'Charset: UTF-8\r\n' + 7 | '\r\n' + 8 | 'xv8AAABSBFPIW9ETCCqGSM49AwEHAgMEh9yJj8tEYplKXKKiTWphXYkJEQSbm0GH\r\n' + 9 | 'hy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3TqVWCNdRpwgcKs3/AAAAFjxx\r\n' + 10 | 'dWFudHN3b3JkQGdtYWlsLmNvbT7C/wAAAGYEEBMIABj/AAAABYJTyFvR/wAAAAmQ\r\n' + 11 | '6bggH1uHbYkAAPefAQDgx/omfDRc7hB4DT1Eong2ytygVXMIuQJmRjnKxqM61AEA\r\n' + 12 | 'g5D6nKw1Woicmg7x2qfj7wU+eLlZ5UXTNqjpe8xQ4+3O/wAAAFYEU8hb0RIIKoZI\r\n' + 13 | 'zj0DAQcCAwS10YFtrIWwvvLE8r32gCEtDD7Cnefkem6Tz4fDFlrdrAUNXADxGLaq\r\n' + 14 | 'AQsgmceluPWjIBY7GtMvd6z/biN8YOANAwEIB8L/AAAAZgQYEwgAGP8AAAAFglPI\r\n' + 15 | 'W9H/AAAACZDpuCAfW4dtiQAAegAA/RYXPbjEOHc7iy3xFxWKWPvpnPc5LwX/6DDt\r\n' + 16 | 'woPMCTLeAQCpjnRiMaIK7tjslDfXd4BtaY6K90JHuRPCQUJ7Uw+fRA==\r\n' + 17 | '=3Iv4\r\n' + 18 | '-----END PGP PUBLIC KEY BLOCK-----\r\n'; 19 | 20 | var keyFingerprint = 'B734 A06E 3413 DD98 6774 3FB3 E9B8 201F 5B87 6D89'; 21 | var keyWords = ["seabird", "confidence", "ragtime", "headwaters", 22 | "choking", "barbecue", "swelter", "narrative", 23 | "freedom", "hydraulic", "cowbell", "pocketful", 24 | "treadmill", "provincial", "bison", "businessman", 25 | "erase", "liberty", "goggles", "matchmaker"]; 26 | 27 | var privateKeyStr = 28 | '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n' + 29 | 'Charset: UTF-8\r\n' + 30 | 'Version: End-To-End v0.3.1338\r\n' + 31 | '\r\n' + 32 | 'xf8AAAB3BFPIW9ETCCqGSM49AwEHAgMEh9yJj8tEYplKXKKiTWphXYkJEQSbm0GH\r\n' + 33 | 'hy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3TqVWCNdRpwgcKgABAIaxz+cn\r\n' + 34 | 'aR1CNIhNGoo7m0T8RycWCslolvmV6JnSFzhYDn3N/wAAABY8cXVhbnRzd29yZEBn\r\n' + 35 | 'bWFpbC5jb20+wv8AAABmBBATCAAY/wAAAAWCU8hb0f8AAAAJkOm4IB9bh22JAAD3\r\n' + 36 | 'nwEA4Mf6Jnw0XO4QeA09RKJ4NsrcoFVzCLkCZkY5ysajOtQBAIOQ+pysNVqInJoO\r\n' + 37 | '8dqn4+8FPni5WeVF0zao6XvMUOPtx/8AAAB7BFPIW9ESCCqGSM49AwEHAgMEtdGB\r\n' + 38 | 'bayFsL7yxPK99oAhLQw+wp3n5Hpuk8+HwxZa3awFDVwA8Ri2qgELIJnHpbj1oyAW\r\n' + 39 | 'OxrTL3es/24jfGDgDQMBCAcAAP40eoOaXxwE/EIXZOddFf+423N12TuuQfqPREhx\r\n' + 40 | 'KOMOAg94wv8AAABmBBgTCAAY/wAAAAWCU8hb0f8AAAAJkOm4IB9bh22JAAB6AAD/\r\n' + 41 | 'R8thL3J2WQsIviAWAZFaip8WCzom60sXCfb3eVC3Eg4BAMR+IehbobVWr3AEdNIj\r\n' + 42 | 'MjSM+cgdhFBqQqQyxFOaX3kRxv8AAABSBFPIW9ETCCqGSM49AwEHAgMEh9yJj8tE\r\n' + 43 | 'YplKXKKiTWphXYkJEQSbm0GHhy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3\r\n' + 44 | 'TqVWCNdRpwgcKs3/AAAAFjxxdWFudHN3b3JkQGdtYWlsLmNvbT7C/wAAAGYEEBMI\r\n' + 45 | 'ABj/AAAABYJTyFvR/wAAAAmQ6bggH1uHbYkAAPefAQDgx/omfDRc7hB4DT1Eong2\r\n' + 46 | 'ytygVXMIuQJmRjnKxqM61AEAg5D6nKw1Woicmg7x2qfj7wU+eLlZ5UXTNqjpe8xQ\r\n' + 47 | '4+3O/wAAAFYEU8hb0RIIKoZIzj0DAQcCAwS10YFtrIWwvvLE8r32gCEtDD7Cnefk\r\n' + 48 | 'em6Tz4fDFlrdrAUNXADxGLaqAQsgmceluPWjIBY7GtMvd6z/biN8YOANAwEIB8L/\r\n' + 49 | 'AAAAZgQYEwgAGP8AAAAFglPIW9H/AAAACZDpuCAfW4dtiQAAegAA/RYXPbjEOHc7\r\n' + 50 | 'iy3xFxWKWPvpnPc5LwX/6DDtwoPMCTLeAQCpjnRiMaIK7tjslDfXd4BtaY6K90JH\r\n' + 51 | 'uRPCQUJ7Uw+fRA==\r\n' + 52 | '=H/6h\r\n' + 53 | '-----END PGP PRIVATE KEY BLOCK-----\r\n'; 54 | 55 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; 56 | 57 | describe('PGP api integration', function() { 58 | 'use strict'; 59 | // Need to ensure freedom is loaded and path is appropriate for environment 60 | var fdom, path; 61 | if (typeof freedom === 'undefined') { 62 | // Node, need to load freedom 63 | fdom = require('freedom-for-node').freedom; 64 | path = '../../build/demo/e2edemo.json'; 65 | } else { 66 | // Chrome or Firefox, assume freedom is already loaded 67 | fdom = freedom; 68 | if (typeof window === 'undefined') { 69 | // Firefox addon, need to prefix path with proper data location 70 | path = 'grunt-jasmine-firefoxaddon-runner/data/build/demo/e2edemo.json'; 71 | } else { 72 | // Chrome app 73 | path = 'scripts/build/demo/e2edemo.json'; 74 | } 75 | } 76 | 77 | beforeEach(function() { 78 | expect(fdom).toBeDefined(); 79 | }); 80 | 81 | it('encrypts, signs, decrypts, verifies', function(done) { 82 | fdom(path).then(function(E2edemo) { 83 | var demo = new E2edemo(); 84 | var msgsReceived = 0; 85 | var expectedMsgs = [ 86 | 'Starting encryption test! Clearing past key...', 87 | 'Exporting public key...', 'Encrypting/signing...', 'Decrypting...', 88 | 'Decrypted!', 'Encryption test SUCCEEDED.']; 89 | demo.on('print', function(msg) { 90 | console.log(msg); 91 | expect(msg).toEqual(expectedMsgs[msgsReceived]); 92 | msgsReceived++; 93 | if (msgsReceived === expectedMsgs.length) { 94 | done(); 95 | } 96 | }); 97 | demo.runCryptoDemo(); 98 | }); 99 | }); 100 | 101 | it('imports existing keypairs', function(done) { 102 | fdom(path).then(function(E2edemo) { 103 | var demo = new E2edemo(); 104 | var msgsReceived = 0; 105 | var expectedMsgs = [ 106 | '', 'Starting keypair import test!', 'Imported keypair...', 107 | 'Fingerprint correct...', 'Fingerprint words correct...', 108 | 'Keypair import test SUCCEEDED.']; 109 | demo.on('print', function(msg) { 110 | console.log(msg); 111 | expect(msg).toEqual(expectedMsgs[msgsReceived]); 112 | msgsReceived++; 113 | if (msgsReceived === expectedMsgs.length) { 114 | done(); 115 | } 116 | }); 117 | demo.runImportDemo(publicKeyStr, privateKeyStr, keyFingerprint, keyWords); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/demo/e2edemo.js: -------------------------------------------------------------------------------- 1 | /*globals freedom,Uint8Array,ArrayBuffer*/ 2 | /*jslint indent:2*/ 3 | 4 | var e2edemo = function (dispatchEvent) { 5 | 'use strict'; 6 | this.dispatch = dispatchEvent; 7 | }; 8 | 9 | e2edemo.prototype.runCryptoDemo = function() { 10 | 'use strict'; 11 | var e2e = new freedom.e2e(); 12 | var plaintext = new ArrayBuffer(12); 13 | var byteView = new Uint8Array(plaintext); 14 | // "123412341234" in ASCII 15 | byteView.set([49, 50, 51, 52, 49, 50, 51, 52, 49, 50, 51, 52]); 16 | 17 | this.dispatch('print', 'Starting encryption test! Clearing past key...'); 18 | return e2e.clear().then(function() { 19 | return e2e.setup('secret passphrase', 'Joe Test '); 20 | }).then( 21 | function() { 22 | this.dispatch('print', 'Exporting public key...'); 23 | return e2e.exportKey(); 24 | }.bind(this)).then(function(publicKey) { 25 | this.dispatch('print', 'Encrypting/signing...'); 26 | return e2e.signEncrypt(plaintext, publicKey.key, true).then( 27 | function(encryptedData) { 28 | this.dispatch('print', 'Decrypting...'); 29 | return e2e.verifyDecrypt(encryptedData, publicKey.key); 30 | }.bind(this)); 31 | }.bind(this)).then(function(result) { 32 | this.dispatch('print', 'Decrypted!'); 33 | var resultView = new Uint8Array(result.data); 34 | if (result.signedBy[0] === 'Joe Test ' && 35 | String.fromCharCode.apply(null, resultView) === 36 | String.fromCharCode.apply(null, byteView)) { 37 | this.dispatch('print', 'Encryption test SUCCEEDED.'); 38 | } else { 39 | this.dispatch('print', 'Encryption test FAILED.'); 40 | } 41 | }.bind(this)).catch( 42 | function(e) { 43 | if (e.message) { 44 | e = e.message; 45 | } 46 | this.dispatch('print', 'Encryption test encountered error: ' + e); 47 | }.bind(this)); 48 | }; 49 | 50 | e2edemo.prototype.runImportDemo = function(publicKeyStr, privateKeyStr, 51 | keyFingerprint, keyWords) { 52 | 'use strict'; 53 | var e2e = new freedom.e2e(); 54 | this.dispatch('print', ''); // blank line to separate from crypto test 55 | this.dispatch('print', 'Starting keypair import test!'); 56 | return e2e.importKeypair('', '', privateKeyStr).then( 57 | function() { 58 | this.dispatch('print', 'Imported keypair...'); 59 | return e2e.getFingerprint(publicKeyStr); 60 | }.bind(this)).then( 61 | function(result) { 62 | if (result.fingerprint === keyFingerprint) { 63 | this.dispatch('print', 'Fingerprint correct...'); 64 | } else { 65 | this.dispatch('print', 'Fingerprint incorrect!'); 66 | } 67 | if (arrayEqual(keyWords, result.words)) { 68 | this.dispatch('print', 'Fingerprint words correct...'); 69 | } else { 70 | this.dispatch('print', 'Fingerprint words incorrect!'); 71 | } 72 | return e2e.exportKey(); 73 | }.bind(this)).then( 74 | function(result) { 75 | if (result.key === publicKeyStr && 76 | result.fingerprint === keyFingerprint && 77 | arrayEqual(keyWords, result.words)) { 78 | this.dispatch('print', 'Keypair import test SUCCEEDED.'); 79 | } else { 80 | this.dispatch('print', 'Keypair import test FAILED.'); 81 | } 82 | }.bind(this)).catch( 83 | function(e) { 84 | if (e.message) { 85 | e = e.message; 86 | } 87 | this.dispatch('print', 'Keypair import test encountered error ' + 88 | e); 89 | }.bind(this)); 90 | }; 91 | 92 | var arrayEqual = function(array1, array2) { 93 | return (array1.length === array2.length) && 94 | array1.every(function(element, index) { 95 | return element === array2[index]; 96 | }); 97 | }; 98 | 99 | freedom().providePromises(e2edemo); 100 | -------------------------------------------------------------------------------- /src/demo/e2edemo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e demo", 3 | "description": "A demo to show e2e usage", 4 | "app": { 5 | "script": "e2edemo.js", 6 | "index": "main.html" 7 | }, 8 | "provides": ["e2edemo"], 9 | "default": "e2edemo", 10 | "api": { 11 | "e2edemo": { 12 | "runCryptoDemo": { 13 | "type": "method", 14 | "value": [], 15 | "ret": [] 16 | }, 17 | "runImportDemo": { 18 | "type": "method", 19 | "value": ["string", "string", "string", ["array", "string"]], 20 | "ret": [] 21 | }, 22 | "print": { 23 | "type": "event", 24 | "value": "string" 25 | } 26 | } 27 | }, 28 | "dependencies": { 29 | "e2e": { 30 | "url": "../pgpapi.json", 31 | "api": "crypto" 32 | } 33 | }, 34 | "permissions": [ 35 | "core.storage" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/demo/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | freedom.js e2e demo 5 | 6 | 7 | 8 | 9 | 10 | To the interactive playground
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/demo/main.js: -------------------------------------------------------------------------------- 1 | /*globals freedom*/ 2 | /*jslint indent:2*/ 3 | 4 | var start = function(E2edemo) { 5 | var publicKeyStr = 6 | '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n' + 7 | 'Charset: UTF-8\r\n' + 8 | '\r\n' + 9 | 'xv8AAABSBFPIW9ETCCqGSM49AwEHAgMEh9yJj8tEYplKXKKiTWphXYkJEQSbm0GH\r\n' + 10 | 'hy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3TqVWCNdRpwgcKs3/AAAAFjxx\r\n' + 11 | 'dWFudHN3b3JkQGdtYWlsLmNvbT7C/wAAAGYEEBMIABj/AAAABYJTyFvR/wAAAAmQ\r\n' + 12 | '6bggH1uHbYkAAPefAQDgx/omfDRc7hB4DT1Eong2ytygVXMIuQJmRjnKxqM61AEA\r\n' + 13 | 'g5D6nKw1Woicmg7x2qfj7wU+eLlZ5UXTNqjpe8xQ4+3O/wAAAFYEU8hb0RIIKoZI\r\n' + 14 | 'zj0DAQcCAwS10YFtrIWwvvLE8r32gCEtDD7Cnefkem6Tz4fDFlrdrAUNXADxGLaq\r\n' + 15 | 'AQsgmceluPWjIBY7GtMvd6z/biN8YOANAwEIB8L/AAAAZgQYEwgAGP8AAAAFglPI\r\n' + 16 | 'W9H/AAAACZDpuCAfW4dtiQAAegAA/RYXPbjEOHc7iy3xFxWKWPvpnPc5LwX/6DDt\r\n' + 17 | 'woPMCTLeAQCpjnRiMaIK7tjslDfXd4BtaY6K90JHuRPCQUJ7Uw+fRA==\r\n' + 18 | '=3Iv4\r\n' + 19 | '-----END PGP PUBLIC KEY BLOCK-----\r\n'; 20 | 21 | var keyFingerprint = 'B734 A06E 3413 DD98 6774 3FB3 E9B8 201F 5B87 6D89'; 22 | var keyWords = ["seabird", "confidence", "ragtime", "headwaters", 23 | "choking", "barbecue", "swelter", "narrative", 24 | "freedom", "hydraulic", "cowbell", "pocketful", 25 | "treadmill", "provincial", "bison", "businessman", 26 | "erase", "liberty", "goggles", "matchmaker"]; 27 | 28 | var privateKeyStr = 29 | '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n' + 30 | 'Charset: UTF-8\r\n' + 31 | 'Version: End-To-End v0.3.1338\r\n' + 32 | '\r\n' + 33 | 'xf8AAAB3BFPIW9ETCCqGSM49AwEHAgMEh9yJj8tEYplKXKKiTWphXYkJEQSbm0GH\r\n' + 34 | 'hy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3TqVWCNdRpwgcKgABAIaxz+cn\r\n' + 35 | 'aR1CNIhNGoo7m0T8RycWCslolvmV6JnSFzhYDn3N/wAAABY8cXVhbnRzd29yZEBn\r\n' + 36 | 'bWFpbC5jb20+wv8AAABmBBATCAAY/wAAAAWCU8hb0f8AAAAJkOm4IB9bh22JAAD3\r\n' + 37 | 'nwEA4Mf6Jnw0XO4QeA09RKJ4NsrcoFVzCLkCZkY5ysajOtQBAIOQ+pysNVqInJoO\r\n' + 38 | '8dqn4+8FPni5WeVF0zao6XvMUOPtx/8AAAB7BFPIW9ESCCqGSM49AwEHAgMEtdGB\r\n' + 39 | 'bayFsL7yxPK99oAhLQw+wp3n5Hpuk8+HwxZa3awFDVwA8Ri2qgELIJnHpbj1oyAW\r\n' + 40 | 'OxrTL3es/24jfGDgDQMBCAcAAP40eoOaXxwE/EIXZOddFf+423N12TuuQfqPREhx\r\n' + 41 | 'KOMOAg94wv8AAABmBBgTCAAY/wAAAAWCU8hb0f8AAAAJkOm4IB9bh22JAAB6AAD/\r\n' + 42 | 'R8thL3J2WQsIviAWAZFaip8WCzom60sXCfb3eVC3Eg4BAMR+IehbobVWr3AEdNIj\r\n' + 43 | 'MjSM+cgdhFBqQqQyxFOaX3kRxv8AAABSBFPIW9ETCCqGSM49AwEHAgMEh9yJj8tE\r\n' + 44 | 'YplKXKKiTWphXYkJEQSbm0GHhy6dQOefg7/uuDMOdI2YF0NLbK+m0sL41Ewfgk/3\r\n' + 45 | 'TqVWCNdRpwgcKs3/AAAAFjxxdWFudHN3b3JkQGdtYWlsLmNvbT7C/wAAAGYEEBMI\r\n' + 46 | 'ABj/AAAABYJTyFvR/wAAAAmQ6bggH1uHbYkAAPefAQDgx/omfDRc7hB4DT1Eong2\r\n' + 47 | 'ytygVXMIuQJmRjnKxqM61AEAg5D6nKw1Woicmg7x2qfj7wU+eLlZ5UXTNqjpe8xQ\r\n' + 48 | '4+3O/wAAAFYEU8hb0RIIKoZIzj0DAQcCAwS10YFtrIWwvvLE8r32gCEtDD7Cnefk\r\n' + 49 | 'em6Tz4fDFlrdrAUNXADxGLaqAQsgmceluPWjIBY7GtMvd6z/biN8YOANAwEIB8L/\r\n' + 50 | 'AAAAZgQYEwgAGP8AAAAFglPIW9H/AAAACZDpuCAfW4dtiQAAegAA/RYXPbjEOHc7\r\n' + 51 | 'iy3xFxWKWPvpnPc5LwX/6DDtwoPMCTLeAQCpjnRiMaIK7tjslDfXd4BtaY6K90JH\r\n' + 52 | 'uRPCQUJ7Uw+fRA==\r\n' + 53 | '=H/6h\r\n' + 54 | '-----END PGP PRIVATE KEY BLOCK-----\r\n'; 55 | 56 | var demo = new E2edemo(); 57 | demo.on('print', function(msg) { 58 | var lines = msg.split('\n'); 59 | for (var i = 0; i < lines.length; i++) { 60 | printToPage(lines[i]); 61 | } 62 | if (lines[0] === 'Encryption test SUCCEEDED.') { 63 | printToPage('\n\n'); 64 | demo.runImportDemo(publicKeyStr, privateKeyStr, keyFingerprint, keyWords); 65 | } 66 | }); 67 | demo.runCryptoDemo(); 68 | }; 69 | 70 | window.onload = function() { 71 | freedom('e2edemo.json', { 72 | 'debug': 'log' 73 | }).then(start); 74 | }; 75 | 76 | function printToPage(msg) { 77 | var logDiv = document.getElementById('log'); 78 | if (typeof msg == 'object') { 79 | logDiv.innerHTML += JSON.stringify(msg) + '
'; 80 | } else { 81 | logDiv.innerHTML += msg + '
'; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/e2e.js: -------------------------------------------------------------------------------- 1 | /*globals freedom, console, e2e, exports, ArrayBuffer, Uint8Array, Uint16Array, DataView*/ 2 | /*jslint indent:2*/ 3 | 4 | var useWebCrypto = false; 5 | if (typeof navigator !== 'undefined' && navigator && navigator.userAgent && 6 | navigator.userAgent.indexOf('Chrome') !== -1) { 7 | // Enable WebCrypto acceleration, on Chrome platforms only. 8 | // This should work on Firefox too, but it doesn't: 9 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1264771 10 | e2e.scheme.EncryptionScheme.WEBCRYPTO_ALGORITHMS = 'ECDH'; 11 | e2e.scheme.SignatureScheme.WEBCRYPTO_ALGORITHMS = 'ECDSA'; 12 | useWebCrypto = true; 13 | } 14 | 15 | // getRandomValue polyfill, currently needed for Firefox webworkers 16 | var refreshBuffer = function (size) { return Promise.resolve(); }; // null-op 17 | if (typeof crypto === 'undefined') { 18 | var rand = freedom['core.crypto'](), 19 | buf, 20 | offset = 0; 21 | refreshBuffer = function (size) { 22 | return rand.getRandomBytes(size).then(function (bytes) { 23 | buf = new Uint8Array(bytes); 24 | offset = 0; 25 | }, function (err) { 26 | console.log(err); 27 | }); 28 | }.bind(this); 29 | 30 | crypto = {}; 31 | crypto.getRandomValues = function (buffer) { 32 | if (buffer.buffer) { 33 | buffer = buffer.buffer; 34 | } 35 | var size = buffer.byteLength, 36 | view = new Uint8Array(buffer), 37 | i; 38 | if (offset + size > buf.length) { 39 | throw new Error("Insufficient Randomness Allocated."); 40 | } 41 | for (i = 0; i < size; i += 1) { 42 | view[i] = buf[offset + i]; 43 | } 44 | offset += size; 45 | }; 46 | } 47 | 48 | /** 49 | * Implementation of a crypto-pgp provider for freedom.js 50 | * using cryptographic code from Google's end-to-end. 51 | **/ 52 | 53 | var mye2e = function(dispatchEvents) { 54 | this.pgpContext = null; 55 | this.pgpUser = null; 56 | }; 57 | 58 | 59 | // These methods implement the actual freedom crypto API. 60 | // Note: This creates a new key if one isn't in storage. 61 | mye2e.prototype.setup = function(passphrase, userid) { 62 | // userid needs to be in format "name " 63 | if (!userid.match(/^[^<]*\s?<[^>]*>$/)) { 64 | return Promise.reject('Invalid userid, expected: "name "'); 65 | } 66 | this.pgpUser = userid; 67 | var scope = this; // jasmine tests fail w/bind approach 68 | return refreshBuffer(5000).then(store.prepareFreedom).then(function() { 69 | if (!scope.pgpContext) { 70 | scope.pgpContext = new e2e.openpgp.ContextImpl(); 71 | scope.pgpContext.armorOutput = false; 72 | } 73 | return scope.pgpContext.initializeKeyRing(passphrase); 74 | }).then(function() { 75 | return scope.pgpContext.searchPrivateKey(scope.pgpUser); 76 | }).then(function(privateKeys) { 77 | if (privateKeys.length === 0) { 78 | var username = scope.pgpUser.slice(0, userid.lastIndexOf('<')).trim(); 79 | var email = scope.pgpUser.slice(userid.lastIndexOf('<') + 1, -1); 80 | return scope.generateKey(username, email); 81 | } 82 | });//.bind(this)); // TODO: switch back to using this once jasmine works 83 | }; 84 | 85 | mye2e.prototype.clear = function() { 86 | // e2e can only store one private key in LocalStorage 87 | // Attempting to set another will result in an HMAC error 88 | // So, make sure to clear before doing so 89 | // See googstorage.js for details on how storage works 90 | return store.prepareFreedom().then(function() { 91 | var storage = new store(); 92 | storage.remove('UserKeyRing'); 93 | }); 94 | }; 95 | 96 | mye2e.prototype.importKeypair = function(passphrase, userid, privateKey) { 97 | var scope = this; // jasmine tests fail w/bind approach 98 | return this.clear().then(function() { 99 | if (!scope.pgpContext) { 100 | scope.pgpContext = new e2e.openpgp.ContextImpl(); 101 | scope.pgpContext.armorOutput = false; 102 | } 103 | scope.pgpContext.setKeyRingPassphrase(passphrase); 104 | return scope.importPrivKey(privateKey, passphrase); 105 | }).then(function() { 106 | return scope.pgpContext.searchPrivateKey(userid); 107 | }).then(function(privateKeys) { 108 | if (privateKeys.length === 0) { 109 | return Promise.reject('Private key does not match provided userid'); 110 | } 111 | return scope.pgpContext.searchPublicKey(userid); 112 | }).then(function(publicKeys) { 113 | if (publicKeys.length === 0) { 114 | return Promise.reject('Public key does not match provided userid'); 115 | } else if (!userid.match(/^[^<]*\s?<[^>]*>$/)) { 116 | return Promise.reject('Invalid userid, expected: "name "'); 117 | } else { 118 | scope.pgpUser = userid; 119 | return Promise.resolve(); 120 | } 121 | }, function(err) { 122 | console.log("Error: ",err); 123 | return Promise.reject(err.toString()); 124 | }); 125 | }; 126 | 127 | mye2e.prototype.exportKey = function(removeHeader) { 128 | // removeHeader is an optional parameter that removes PGP header and newlines 129 | return this.pgpContext.searchPublicKey(this.pgpUser).then( 130 | function(keyResult) { 131 | var serialized = keyResult[0].serialized; 132 | var key = e2e.openpgp.asciiArmor.encode('PUBLIC KEY BLOCK', serialized); 133 | if (removeHeader) { 134 | // If optional removeHeader is true, then remove the PGP head/foot lines 135 | key = key.split('\r\n').slice(3, key.split('\r\n').length - 2).join(''); 136 | } 137 | return { 138 | 'key': key, 139 | 'fingerprint': keyResult[0].key.fingerprintHex, 140 | 'words': hex2words(keyResult[0].key.fingerprintHex) 141 | }; 142 | }); 143 | }; 144 | 145 | mye2e.prototype.getFingerprint = function(publicKey) { 146 | // Returns v4 fingerprint per RFC 4880 Section 12.2 147 | // http://tools.ietf.org/html/rfc4880#section-12.2 148 | return this.pgpContext.getKeyDescription(publicKey).then( 149 | function(keyDescriptions) { 150 | var fingerprint = keyDescriptions[0].key.fingerprintHex; 151 | return { 152 | 'fingerprint': fingerprint, 153 | 'words': hex2words(fingerprint) 154 | }; 155 | }); 156 | }; 157 | 158 | mye2e.prototype.signEncrypt = function(data, encryptKey, sign) { 159 | var scope = this; 160 | var pgp = this.pgpContext; 161 | var user = this.pgpUser; 162 | return refreshBuffer(5000).then(function () { 163 | if (typeof sign === 'undefined') { 164 | sign = true; 165 | } 166 | var keys = Promise.resolve([]); 167 | if (encryptKey) { 168 | keys = scope.importPubKey(encryptKey).then(function(key) { 169 | return [key]; 170 | }); 171 | } 172 | return keys; 173 | }).then(function(keys) { 174 | var signKeysPromise = Promise.resolve([null]); 175 | if (sign) { 176 | signKeysPromise = pgp.searchPrivateKey(user); 177 | } 178 | return signKeysPromise.then(function(signKeys) { 179 | return pgp.encryptSign(buf2array(data), [], keys, [], signKeys[0]); 180 | }); 181 | }).then(array2buf); 182 | }; 183 | 184 | mye2e.prototype.verifyDecrypt = function(data, verifyKey) { 185 | var importedKey; 186 | if (typeof verifyKey === 'undefined') { 187 | verifyKey = ''; 188 | importedKey = Promise.resolve(); 189 | } else { 190 | importedKey = this.importPubKey(verifyKey); 191 | } 192 | var byteView = new Uint8Array(data); 193 | var pgp = this.pgpContext; 194 | return importedKey.then(function() { 195 | return pgp.verifyDecrypt(function () { 196 | throw new Error('Passphrase decryption is not supported'); 197 | }, buf2array(data)); 198 | }).then(function (result) { 199 | var signed = null; 200 | if (verifyKey) { 201 | signed = result.verify.success[0].uids; 202 | } 203 | return { 204 | data: array2buf(result.decrypt.data), 205 | signedBy: signed 206 | }; 207 | }); 208 | }; 209 | 210 | mye2e.prototype.armor = function(data, type) { 211 | if (typeof type === 'undefined') { 212 | type = 'MESSAGE'; 213 | } 214 | var byteView = new Uint8Array(data); 215 | return Promise.resolve(e2e.openpgp.asciiArmor.encode(type, byteView)); 216 | }; 217 | 218 | mye2e.prototype.dearmor = function(data) { 219 | return Promise.resolve(array2buf(e2e.openpgp.asciiArmor.parse(data).data)); 220 | }; 221 | 222 | // Basic EC Diffie-Hellman shared secret calculation. 223 | // 224 | // 'curveName' is a simple string identifying the ECC curve. "P_256" is 225 | // a lovely value. 226 | // 'peerPubKey' is expected to be an armored key like "-----BEGIN PGP 227 | // PUBLIC KEY BLOCK...". 228 | mye2e.prototype.ecdhBob = function(curveName, peerPubKey) { 229 | if (!(curveName in e2e.ecc.PrimeCurve)) { 230 | return Promise.reject('Invalid Prime Curve'); 231 | } 232 | try { 233 | // Base call in this c'tor throws. 234 | var ecdh = new e2e.ecc.Ecdh(curveName); 235 | var parsedPubkey = e2e.openpgp.block.factory.parseByteArrayTransferableKey( 236 | e2e.openpgp.asciiArmor.parse(peerPubKey).data); 237 | var pubkey = parsedPubkey.keyPacket.cipher.ecdsa_.getPublicKey(); 238 | 239 | var keyRing = this.pgpContext.keyRing_; 240 | var privKey = keyRing.searchKey(this.pgpUser, e2e.openpgp.KeyRing.Type.PRIVATE); 241 | return keyRing.getKeyBlock(privKey[0].toKeyObject()) 242 | .then(function(localPrivKey) { 243 | var cipher = localPrivKey.keyPacket.cipher; 244 | 245 | // The curve data in both cases are simple arrays of numbers, so 246 | // this works pretty well. 247 | if (cipher.cipher_.key.curve.toString() != 248 | parsedPubkey.keyPacket.cipher.key.curve.toString()) { 249 | throw new Error('Keys have different curves.'); 250 | } 251 | var wrap = cipher.getWrappedCipher(); 252 | var bobResult = ecdh.bob(pubkey, wrap.key.privKey); 253 | return array2buf(bobResult.secret); 254 | }); 255 | } catch (e) { 256 | console.log("ERROR: " + JSON.stringify(e)); 257 | console.log(e); 258 | console.log(e.stack); 259 | return Promise.reject(e); 260 | } 261 | }; 262 | 263 | // The following methods are part of the prototype to be able to access state 264 | // but are not part of the API and should not be exposed to the client 265 | mye2e.prototype.generateKey = function(name, email) { 266 | var pgp = this.pgpContext; 267 | var expiration = Date.now() / 1000 + (3600 * 24 * 365); 268 | var location = useWebCrypto ? 'WEB_CRYPTO' : 'JAVASCRIPT'; 269 | return pgp.generateKey('ECDSA', 256, 'ECDH', 256, name, '', email, expiration, 270 | location) 271 | .then(function (keys) { 272 | if (keys.length !== 2) { 273 | throw new Error('Failed to generate key'); 274 | } 275 | }); 276 | }; 277 | 278 | mye2e.prototype.deleteKey = function(uid) { 279 | this.pgpContext.deleteKey(uid); 280 | return Promise.resolve(); 281 | }; 282 | 283 | mye2e.prototype.importPrivKey = function(keyStr, passphrase) { 284 | if (typeof passphrase === 'undefined') { 285 | passphrase = ''; 286 | } 287 | var pgp = this.pgpContext; 288 | return pgp.importKey( 289 | function(str) { 290 | return e2e.async.Result.toResult(passphrase); 291 | }, keyStr); 292 | }; 293 | 294 | mye2e.prototype.importPubKey = function(keyStr) { 295 | // Algorithm: 296 | // 1. Compute the key description, which includes the fingerprint. 297 | // This action has no side effects (does not import the key). 298 | // 2. Import the key. This returns the "uid", i.e. e-mail address. 299 | // 3. Search for all known public keys with this uid. 300 | // 4. Find the key whose fingerprint matches the input. Return this one. 301 | var pgp = this.pgpContext; 302 | return pgp.getKeyDescription(keyStr).then(function(keyDescriptions) { 303 | var keyDescription = keyDescriptions[0]; 304 | return pgp.importKey(function(str) { 305 | throw new Error('No passphrase needed for a public key'); 306 | }, keyStr).then(function(uids) { 307 | if (uids.length !== 1) throw new Error('too many uids'); 308 | return pgp.searchPublicKey(uids[0]); 309 | }).then(function(candidateKeydescriptions) { 310 | var rightKey = null; 311 | candidateKeydescriptions.forEach(function(candidateKeyDescription) { 312 | if (candidateKeyDescription.key.fingerprintHex === 313 | keyDescription.key.fingerprintHex) { 314 | rightKey = candidateKeyDescription; 315 | } 316 | }); 317 | if (!rightKey) throw new Error('could not import key'); 318 | return rightKey; 319 | }); 320 | }); 321 | }; 322 | 323 | mye2e.prototype.searchPrivateKey = function(uid) { 324 | var pgp = this.pgpContext; 325 | return new Promise( 326 | function(resolve, reject) { 327 | pgp.searchPrivateKey(uid).addCallback(resolve).addErrback(reject); 328 | }); 329 | }; 330 | 331 | mye2e.prototype.searchPublicKey = function(uid) { 332 | var pgp = this.pgpContext; 333 | return new Promise( 334 | function(resolve, reject) { 335 | pgp.searchPublicKey(uid).addCallback(resolve).addErrback(reject); 336 | }); 337 | }; 338 | 339 | // Helper methods (that don't need state and could be moved elsewhere) 340 | function array2str(a) { 341 | var str = ''; 342 | for (var i = 0; i < a.length; i++) { 343 | str += String.fromCharCode(a[i]); 344 | } 345 | return str; 346 | } 347 | 348 | function str2buf(s) { 349 | var buf = new ArrayBuffer(s.length * 2); 350 | var view = new Uint16Array(buf); 351 | for (var i = 0; i < s.length; i++) { 352 | view[i] = s.charCodeAt(i); 353 | } 354 | return buf; 355 | } 356 | 357 | function array2buf(a) { 358 | var buf = new ArrayBuffer(a.length); 359 | var byteView = new Uint8Array(buf); 360 | byteView.set(a); 361 | return buf; 362 | } 363 | 364 | function buf2array(b) { 365 | var dataView = new DataView(b); 366 | var result = []; 367 | for (var i = 0; i < dataView.byteLength; i++) { 368 | result.push(dataView.getUint8(i)); 369 | } 370 | return result; 371 | } 372 | 373 | if (typeof freedom !== 'undefined') { 374 | freedom().providePromises(mye2e); 375 | } 376 | if (typeof exports !== 'undefined') { 377 | exports.mye2e = mye2e; 378 | } 379 | -------------------------------------------------------------------------------- /src/googstorage.js: -------------------------------------------------------------------------------- 1 | 2 | function store () { 3 | this.freedomStorage = freedom['core.storage'](); 4 | this.memStorage = {}; 5 | this.initialize(); 6 | } 7 | 8 | store.prototype.set = function(key, val) { 9 | if (val === undefined) { 10 | this.freedomStorage.remove(key); 11 | delete this.memStorage[key]; 12 | return val; 13 | } 14 | this.freedomStorage.set(key, val); 15 | this.memStorage[key] = val; 16 | return val; 17 | }; 18 | 19 | store.prototype.get = function(key) { 20 | return this.memStorage[key]; 21 | }; 22 | 23 | store.prototype.remove = function(key) { 24 | delete this.memStorage[key]; 25 | this.freedomStorage.remove(key); 26 | }; 27 | 28 | store.prototype.clear = function() { 29 | this.memStorage = {}; 30 | this.freedomStorage.clear(); 31 | }; 32 | 33 | store.prototype.transact = function(key, defaultVal, transactionFn) { 34 | var val = this.memStorage.get(key); 35 | if (transactionFn === null) { 36 | transactionFn = defaultVal; 37 | defaultVal = null; 38 | } 39 | if (typeof val === 'undefined') { 40 | val = defaultVal || {}; 41 | } 42 | transactionFn(val); 43 | this.set(key, val); 44 | }; 45 | 46 | store.prototype.getAll = function() { return this.memStorage; }; 47 | 48 | store.prototype.forEach = function(callback) { 49 | for (var i in this.memStorage) { 50 | callback(i); 51 | } 52 | }; 53 | 54 | store.prototype.serialize = function(value) { 55 | return JSON.stringify(value); 56 | }; 57 | 58 | store.prototype.deserialize = function(value) { 59 | if (typeof value !== 'string') { 60 | return undefined; 61 | } 62 | try { 63 | return JSON.parse(value); 64 | } catch(e) { 65 | return value || undefined; 66 | } 67 | }; 68 | 69 | store.prototype.initialize = function() { 70 | if (!store.isPrepared) { 71 | throw new Error('store is not yet prepared'); 72 | } 73 | this.memStorage = store.preparedMem; 74 | }; 75 | 76 | store.preparedMem = {}; 77 | store.isPrepared = false; 78 | 79 | // IMPORTANT - this function must be called and resolved before instantiating 80 | // a store object, otherwise async nastiness w/freedom localStorage occurs 81 | store.prepareFreedom = function() { 82 | return freedom['core.storage']().get('UserKeyRing').then(function(value) { 83 | if (value) { 84 | store.preparedMem.UserKeyRing = value; 85 | } 86 | store.isPrepared = true; 87 | }); 88 | }; 89 | 90 | goog.storage.mechanism.HTML5LocalStorage = store; 91 | -------------------------------------------------------------------------------- /src/googstorage_mock.js: -------------------------------------------------------------------------------- 1 | function store () { 2 | this.storage = {}; 3 | if (!store.isPrepared) { 4 | throw new Error('store not yet prepared'); 5 | } 6 | } 7 | 8 | store.prototype.set = function(key, val) { 9 | if (val === undefined) { 10 | delete this.storage[key]; 11 | return val; 12 | } 13 | this.storage[key] = val; 14 | return val; 15 | }; 16 | 17 | store.prototype.get = function(key) { return this.storage[key]; }; 18 | 19 | store.prototype.remove = function(key) { delete this.storage[key];}; 20 | 21 | store.prototype.clear = function() { this.storage = {}; }; 22 | 23 | store.prototype.transact = function(key, defaultVal, transactionFn) { 24 | var val = this.store.get(key); 25 | if (transactionFn === null) { 26 | transactionFn = defaultVal; 27 | defaultVal = null; 28 | } 29 | if (typeof val == 'undefined') { 30 | val = defaultVal || {}; 31 | } 32 | transactionFn(val); 33 | this.set(key, val); 34 | }; 35 | 36 | store.prototype.getAll = function() { 37 | return this.storage; 38 | }; 39 | 40 | store.prototype.forEach = function(callback) { 41 | for (var i in this.storage) { 42 | callback(i); 43 | } 44 | }; 45 | 46 | store.prototype.serialize = function(value) { 47 | return JSON.stringify(value); 48 | }; 49 | 50 | store.prototype.deserialize = function(value) { 51 | if (typeof value != 'string') { return undefined; } 52 | try { return JSON.parse(value); } 53 | catch(e) { return value || undefined; } 54 | }; 55 | 56 | store.isPrepared = false; 57 | 58 | store.prepareFreedom = function() { 59 | // mock doesn't actually use freedom localStorage 60 | store.isPrepared = true; 61 | return Promise.resolve(); 62 | }; 63 | 64 | goog.storage.mechanism.HTML5LocalStorage = store; 65 | -------------------------------------------------------------------------------- /src/pgpapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "description": "End-to-end Freedom manifest", 4 | "app": { 5 | "script": [ 6 | "end-to-end.compiled.js", 7 | "googstorage.js", 8 | "hex2words.js", 9 | "e2e.js" 10 | ] 11 | }, 12 | "constraints": { 13 | "isolation": "never" 14 | }, 15 | "provides" : [ 16 | "crypto" 17 | ], 18 | "default": "crypto", 19 | "api": { 20 | "crypto": { 21 | "_comment": "Simplified crypto interface", 22 | 23 | "ERRCODE": { "type": "constant", "value": { 24 | "KEYS_NOT_LOADED": "No keys in memory - initialize first", 25 | "MALFORMED": "Malformed armored message", 26 | "INVALID_PASSPHRASE": "Wrong passphrase", 27 | "KEY_NOT_GENERATED": "Trying to generate key, but it already exists", 28 | "KEY_DOES_NOT_EXIST": "Trying to load a key that's not there", 29 | "BAD_SIGNATURE": "Invalid signature on call to verify" 30 | }}, 31 | 32 | "setup": { 33 | "_comment": "Creates a new keypair or loads one if it already exists", 34 | "type": "method", 35 | "value": ["string", "string"], 36 | "ret": [], 37 | "err": { "errcode": "string", "message": "string" } 38 | }, 39 | 40 | "clear": { 41 | "_comment": "Clear local storage, resetting private key", 42 | "type": "method", 43 | "value": [], 44 | "ret": [], 45 | "err": { "errcode": "string", "message": "string" } 46 | }, 47 | 48 | "importKeypair": { 49 | "_comment": "Import a public/private keypair, overwriting if needed", 50 | "type": "method", 51 | "value": ["string", "string", "string"], 52 | "ret": [], 53 | "err": { "errcode": "string", "message": "string" } 54 | }, 55 | 56 | "exportKey": { 57 | "_comment": "Export your public key and fingerprint to share/check", 58 | "type": "method", 59 | "value": ["boolean"], 60 | "ret": { "key": "string", "fingerprint": "string", 61 | "words": ["array", "string"] }, 62 | "err": { "errcode": "string", "message": "string" } 63 | }, 64 | 65 | "getFingerprint": { 66 | "_comment": "Calculate and return 20-byte public key fingerprint", 67 | "type": "method", 68 | "value": ["string"], 69 | "ret": { "fingerprint": "string", "words": ["array", "string"] }, 70 | "err": { "errcode": "string", "message": "string" } 71 | }, 72 | 73 | "signEncrypt": { 74 | "_comment": "Sign w/private key, optionally encrypt with other key", 75 | "type": "method", 76 | "value": ["buffer", "string", "boolean"], 77 | "ret": "buffer", 78 | "err": { "errcode": "string", "message": "string" } 79 | }, 80 | 81 | "verifyDecrypt": { 82 | "_comment": "Decrypt w/private key, optionally verify with other key", 83 | "type": "method", 84 | "value": ["buffer", "string"], 85 | "ret": { "data": "buffer", "signedBy": ["array", "string"] }, 86 | "err": { "errcode": "string", "message": "string" } 87 | }, 88 | 89 | "armor": { 90 | "_comment": "ASCII armor data, and include given header", 91 | "type": "method", 92 | "value": ["buffer", "string"], 93 | "ret": "string", 94 | "err": { "errcode": "string", "message": "string" } 95 | }, 96 | 97 | "dearmor": { 98 | "_comment": "De-armor given data, checking for given header", 99 | "type": "method", 100 | "value": ["string"], 101 | "ret": "buffer", 102 | "err": { "errcode": "string", "message": "string" } 103 | }, 104 | 105 | "ecdhBob": { 106 | "_comment": "Full elliptical curve implementation in a single function", 107 | "type": "method", 108 | "value": ["string", "string"], 109 | "ret": "buffer", 110 | "err": { "errcode": "string", "message": "string" } 111 | } 112 | } 113 | }, 114 | 115 | "permissions": [ 116 | "core.crypto", 117 | "core.storage" 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /src/playground/README.md: -------------------------------------------------------------------------------- 1 | PGP Playground 2 | ============== 3 | 4 | This is an interactive demo to demonstrate the different APIs presented by the 5 | crypto API, and understand the transformations they produce. 6 | -------------------------------------------------------------------------------- /src/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PGP ~ freedom.js Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 100 | 101 | 102 | 103 |
Demos
104 |
PGP
105 |
106 | 107 | 108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 |
117 |

Output

118 |
119 |     
120 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /src/playground/style.css: -------------------------------------------------------------------------------- 1 | @import url("//fonts.googleapis.com/css?family=Rosario:700i"); 2 | 3 | body { 4 | font: 16px/26px "Roboto", "Helvetica Neue", Helvetica, arial, sans-serif; 5 | color: #444; 6 | -webkit-font-smoothing: antialiased; 7 | font-smoothing: antialiased; 8 | font-weight: 300; 9 | } 10 | 11 | #logo, .freedomjs { 12 | font-family: 'Rosario', sans-serif; 13 | font-weight: 700; 14 | font-style: italic; 15 | color: rgb(253, 60, 0); 16 | } 17 | 18 | .big { 19 | font-size: 1.5em; 20 | } 21 | 22 | #logo { 23 | padding-left: 1.2em; 24 | } 25 | 26 | header { 27 | padding-top: 0.5em; 28 | font-size: 2em; 29 | font-weight: 200; 30 | display: inline-block; 31 | } 32 | 33 | .triangle { 34 | padding-left: 0.5em; 35 | font-size: 85%; 36 | color: #666; 37 | } 38 | 39 | ul { 40 | list-style: none; 41 | } 42 | 43 | li > a { 44 | color: #444; 45 | display: block; 46 | font-weight: 600; 47 | } 48 | 49 | li > kbd { 50 | display: block; 51 | } 52 | 53 | li { 54 | padding-bottom: 1em; 55 | } 56 | 57 | kbd a { 58 | color: #444; 59 | font-weight: 100; 60 | font-size: 0.8em; 61 | } 62 | 63 | kbd:before { 64 | content: ">"; 65 | } 66 | 67 | button { 68 | background: transparent; 69 | border: 1px solid; 70 | border-color: #eee #ccc #ccc #eee; 71 | font-size: 100%; 72 | outline: none; 73 | } 74 | 75 | button:active { 76 | background: #eee; 77 | } 78 | 79 | .lastChild > * { 80 | display: none; 81 | } 82 | 83 | .lastChild > *:last-child { 84 | display:block; 85 | } 86 | -------------------------------------------------------------------------------- /src/test_mock.js: -------------------------------------------------------------------------------- 1 | goog.array.extend=function(a,b){ 2 | for(var c=1;c