Bläddra i källkod

implementing web crypto API for encryption

El RIDO 8 år sedan
förälder
incheckning
0dbbb61d11
10 ändrade filer med 282 tillägg och 82 borttagningar
  1. 3 3
      .travis.yml
  2. 2 2
      composer.json
  3. 1 0
      js/common.js
  4. 43 0
      js/package.json
  5. 207 63
      js/privatebin.js
  6. 14 7
      js/test/CryptTool.js
  7. 4 5
      js/test/Helper.js
  8. 1 1
      tpl/bootstrap.php
  9. 1 1
      tpl/page.php
  10. 6 0
      tst/README.md

+ 3 - 3
.travis.yml

@@ -10,13 +10,13 @@ php:
 
 # as this is a php project, node.js v4 (for JS unit testing) isn't installed
 install:
-  - if [ ! -d "$HOME/.nvm" ]; then mkdir -p $HOME/.nvm && curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | NVM_METHOD=script bash; fi
-  - source ~/.nvm/nvm.sh && nvm install 4
+  - if [ ! -d "$HOME/.nvm" ]; then mkdir -p $HOME/.nvm && curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | NVM_METHOD=script bash; fi
+  - source ~/.nvm/nvm.sh && nvm install --lts
 
 before_script:
   - composer install -n
   - npm install -g mocha
-  - cd js && npm install jsverify jsdom@9 jsdom-global@2 mime-types
+  - cd js && npm install
 
 script:
   - mocha

+ 2 - 2
composer.json

@@ -3,13 +3,13 @@
     "description": "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted in the browser using 256 bit AES in Galois Counter mode (GCM).",
     "type": "project",
     "keywords": ["private", "secure", "end-to-end-encrypted", "e2e", "paste", "pastebin", "zero", "zero-knowledge", "encryption", "encrypted", "AES"],
-    "homepage": "https://github.com/PrivateBin",
+    "homepage": "https://privatebin.info/",
     "license":"zlib-acknowledgement",
     "support": {
         "issues": "https://github.com/PrivateBin/PrivateBin/issues",
         "wiki": "https://github.com/PrivateBin/PrivateBin/wiki",
         "source": "https://github.com/PrivateBin/PrivateBin",
-        "docs": "https://zerobin.dssr.ch/documentation/"
+        "docs": "https://privatebin.info/codedoc/"
     },
     "require": {
         "php": "^5.4.0 || ^7.0",

+ 1 - 0
js/common.js

@@ -6,6 +6,7 @@ global.jsc = require('jsverify');
 global.jsdom = require('jsdom-global');
 global.cleanup = global.jsdom();
 global.fs = require('fs');
+global.WebCrypto = require('node-webcrypto-ossl');
 
 // application libraries to test
 global.$ = global.jQuery = require('./jquery-3.3.1');

+ 43 - 0
js/package.json

@@ -0,0 +1,43 @@
+{
+  "name": "privatebin",
+  "version": "1.2.1",
+  "description": "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted in the browser using 256 bit AES in Galois Counter mode (GCM).",
+  "main": "privatebin.js",
+  "directories": {
+    "test": "test"
+  },
+  "dependencies": {},
+  "devDependencies": {
+    "jsdom": "^9.12.0",
+    "jsdom-global": "^2.1.1",
+    "jsverify": "^0.8.3",
+    "mime-types": "^2.1.20",
+    "node-webcrypto-ossl": "^1.0.37"
+  },
+  "scripts": {
+    "test": "mocha"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/PrivateBin/PrivateBin.git"
+  },
+  "keywords": [
+    "private",
+    "secure",
+    "end-to-end-encrypted",
+    "e2e",
+    "paste",
+    "pastebin",
+    "zero",
+    "zero-knowledge",
+    "encryption",
+    "encrypted",
+    "AES"
+  ],
+  "author": "",
+  "license": "zlib-acknowledgement",
+  "bugs": {
+    "url": "https://github.com/PrivateBin/PrivateBin/issues"
+  },
+  "homepage": "https://privatebin.info/"
+}

+ 207 - 63
js/privatebin.js

@@ -526,6 +526,30 @@ jQuery.PrivateBin = (function($, sjcl, RawDeflate) {
     var CryptTool = (function () {
         var me = {};
 
+        /**
+         * convert UTF-8 string stored in a DOMString to a standard UTF-16 DOMString
+         *
+         * Iterates over the bytes of the message, converting them all hexadecimal
+         * percent encoded representations, then URI decodes them all
+         *
+         * @name   CryptTool.btou
+         * @function
+         * @private
+         * @param  {string} message UTF-8 string
+         * @return {string} UTF-16 string
+         */
+        function btou(message)
+        {
+            return decodeURIComponent(
+                message.split('').map(
+                    function(character)
+                    {
+                        return '%' + ('00' + character.charCodeAt(0).toString(16)).slice(-2);
+                    }
+                ).join('')
+            );
+        }
+
         /**
          * convert DOMString (UTF-16) to a UTF-8 string stored in a DOMString
          *
@@ -550,27 +574,49 @@ jQuery.PrivateBin = (function($, sjcl, RawDeflate) {
         }
 
         /**
-         * convert UTF-8 string stored in a DOMString to a standard UTF-16 DOMString
+         * convert ArrayBuffer into a UTF-8 string
          *
-         * Iterates over the bytes of the message, converting them all hexadecimal
-         * percent encoded representations, then URI decodes them all
+         * Iterates over the bytes of the array, catenating them into a string
          *
-         * @name   CryptTool.btou
+         * @name   CryptTool.ArrToStr
+         * @function
+         * @private
+         * @param  {ArrayBuffer} messageArray
+         * @return {string} message
+         */
+        function ArrToStr(messageArray)
+        {
+            var array = new Uint8Array(messageArray),
+                len = array.length,
+                message = '',
+                i = 0;
+            while(i < len) {
+                var c = array[i++];
+                message += String.fromCharCode(c);
+            }
+            return message;
+        }
+
+        /**
+         * convert UTF-8 string into a Uint8Array
+         *
+         * Iterates over the bytes of the message, writing them to the array
+         *
+         * @name   CryptTool.StrToArr
          * @function
          * @private
          * @param  {string} message UTF-8 string
-         * @return {string} UTF-16 string
+         * @return {Uint8Array} array
          */
-        function btou(message)
+        function StrToArr(message)
         {
-            return decodeURIComponent(
-                message.split('').map(
-                    function(character)
-                    {
-                        return '%' + ('00' + character.charCodeAt(0).toString(16)).slice(-2);
-                    }
-                ).join('')
-            );
+            var messageUtf8  = message,
+                messageLen   = messageUtf8.length,
+                messageArray = new Uint8Array(messageLen);
+            for (var i = 0; i < messageLen; ++i) {
+                messageArray[i] = messageUtf8.charCodeAt(i);
+            }
+            return messageArray;
         }
 
         /**
@@ -611,6 +657,42 @@ jQuery.PrivateBin = (function($, sjcl, RawDeflate) {
             }
         }
 
+        /**
+         * returns specified number of random bytes
+         *
+         * @name   CryptTool.getRandomBytes
+         * @function
+         * @private
+         * @param  {int} length number of random bytes to fetch
+         * @throws {string}
+         * @return {string} random bytes
+         */
+        function getRandomBytes(length)
+        {
+            if (
+                typeof window !== 'undefined' &&
+                typeof Uint8Array !== 'undefined' &&
+                String.fromCodePoint &&
+                (
+                    typeof window.crypto !== 'undefined' ||
+                    typeof window.msCrypto !== 'undefined'
+                )
+            ) {
+                // modern browser environment
+                var bytes = '',
+                    byteArray = new Uint8Array(length),
+                    crypto = window.crypto || window.msCrypto;
+                crypto.getRandomValues(byteArray);
+                for (var i = 0; i < length; ++i) {
+                    bytes += String.fromCharCode(byteArray[i]);
+                }
+                return bytes;
+            } else {
+                // legacy browser or unsupported environment
+                throw 'No supported crypto API detected, you may read pastes and comments, but can\'t create pastes or add new comments.';
+            }
+        };
+
         /**
          * compress, then encrypt message with given key and password
          *
@@ -621,19 +703,68 @@ jQuery.PrivateBin = (function($, sjcl, RawDeflate) {
          * @param  {string} message
          * @return {string} data - JSON with encrypted data
          */
-        me.cipher = function(key, password, message)
-        {
-            // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
-            var options = {
-                mode: 'gcm',
-                ks: 256,
-                ts: 128
-            };
-
-            if ((password || '').trim().length === 0) {
-                return sjcl.encrypt(key, compress(message), options);
+        me.cipher = async function(key, password, message)
+        {
+            // AES in Galois Counter Mode, keysize 256 bit, authentication tag 128 bit, 10000 iterations in key derivation
+            var iv     = getRandomBytes(16),
+                salt   = getRandomBytes(8),
+                object = {
+                    iv:     btoa(iv),
+                    v:      1,
+                    iter:   10000,
+                    ks:     256,
+                    ts:     128,
+                    mode:   'gcm',
+                    adata:  '', // if used, base64 encode it with btoa()
+                    cipher: 'aes',
+                    salt:   btoa(salt)
+                },
+                algo   = 'AES-' + object.mode.toUpperCase();
+
+            if ((password || '').trim().length > 0) {
+                key += sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password));
             }
-            return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), compress(message), options);
+
+            // import raw key
+            var importedKey = await window.crypto.subtle.importKey(
+                'raw', // only 'raw' is allowed
+                StrToArr(key),
+                {name: 'PBKDF2'}, // we use PBKDF2 for key derivation
+                false, // the key may not be exported
+                ["deriveKey"] // we may only use it for key derivation
+            )
+
+            // derive a stronger key for use with AES
+            var derivedKey = await window.crypto.subtle.deriveKey(
+                {
+                    name: 'PBKDF2', // we use PBKDF2 for key derivation
+                    salt: StrToArr(atob(object.salt)), // salt used in HMAC
+                    iterations: object.iter, // amount of iterations to apply
+                    hash: {name: "SHA-256"}, // can be "SHA-1", "SHA-256", "SHA-384" or "SHA-512"
+                },
+                importedKey,
+                {
+                    // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC")
+                    name: algo,
+                    length: object.ks, // can be 128, 192 or 256
+                },
+                false, // the key may not be exported
+                ["encrypt"] // we may only use it for decryption
+            )
+
+            // finally, encrypt message
+            var encrypted = await window.crypto.subtle.encrypt(
+                {
+                    // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC")
+                    name: algo,
+                    iv: StrToArr(atob(object.iv)), // the initialization vector you used to encrypt
+                    additionalData: StrToArr(atob(object.adata)), // the addtional data you used during encryption (if any)
+                    tagLength: object.ts, // the length of the tag you used to encrypt (if any)
+                },
+                derivedKey,
+                StrToArr(compress(message)) // compressed plain text to encrypt
+            )
+            return btoa(ArrToStr(encrypted));
         };
 
         /**
@@ -646,18 +777,57 @@ jQuery.PrivateBin = (function($, sjcl, RawDeflate) {
          * @param  {string} data - JSON with encrypted data
          * @return {string} decrypted message, empty if decryption failed
          */
-        me.decipher = function(key, password, data)
+        me.decipher = async function(key, password, data)
         {
-            if (data !== undefined) {
-                try {
-                    return decompress(sjcl.decrypt(key, data));
-                } catch(err) {
-                    try {
-                        return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
-                    } catch(e) {
-                        return '';
-                    }
+            try {
+                if (password.length > 0) {
+                    key += sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password));
                 }
+                var object = JSON.parse(data),
+                    algo   = 'AES-' + object.mode.toUpperCase();
+
+                // import raw key
+                var importedKey = await window.crypto.subtle.importKey(
+                    'raw', // only 'raw' is allowed
+                    StrToArr(key),
+                    {name: 'PBKDF2'}, // we use PBKDF2 for key derivation
+                    false, // the key may not be exported
+                    ["deriveKey"] // we may only use it for key derivation
+                )
+
+                // derive a stronger key for use with AES
+                var derivedKey = await window.crypto.subtle.deriveKey(
+                    {
+                        name: 'PBKDF2', // we use PBKDF2 for key derivation
+                        salt: StrToArr(atob(object.salt)), // salt used in HMAC
+                        iterations: object.iter, // amount of iterations to apply
+                        hash: {name: "SHA-256"}, // can be "SHA-1", "SHA-256", "SHA-384" or "SHA-512"
+                    },
+                    importedKey,
+                    {
+                        // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC")
+                        name: algo,
+                        length: object.ks, // can be 128, 192 or 256
+                    },
+                    false, // the key may not be exported
+                    ["decrypt"] // we may only use it for decryption
+                )
+
+                // finally, decrypt message
+                var decrypted = await window.crypto.subtle.decrypt(
+                    {
+                        // can be any supported AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH" or "HMAC")
+                        name: algo,
+                        iv: StrToArr(atob(object.iv)), // the initialization vector you used to encrypt
+                        additionalData: StrToArr(atob(object.adata)), // the addtional data you used during encryption (if any)
+                        tagLength: object.ts, // the length of the tag you used to encrypt (if any)
+                    },
+                    derivedKey,
+                    StrToArr(atob(object.ct)) // cipher text to decrypt
+                )
+                return decompress(ArrToStr(decrypted));
+            } catch(err) {
+                return '';
             }
         };
 
@@ -673,33 +843,7 @@ jQuery.PrivateBin = (function($, sjcl, RawDeflate) {
          */
         me.getSymmetricKey = function()
         {
-            var crypto, key;
-            if (typeof module !== 'undefined' && module.exports) {
-                // node environment
-                key = require('crypto').randomBytes(32).toString('base64');
-            } else if (
-                typeof window !== 'undefined' &&
-                typeof Uint8Array !== 'undefined' &&
-                String.fromCodePoint &&
-                (
-                    typeof window.crypto !== 'undefined' ||
-                    typeof window.msCrypto !== 'undefined'
-                )
-            ) {
-                // modern browser environment
-                var bytes = '',
-                    byteArray = new Uint8Array(32),
-                    crypto = window.crypto || window.msCrypto;
-                crypto.getRandomValues(byteArray);
-                for (var i = 0; i < 32; ++i) {
-                    bytes += String.fromCharCode(byteArray[i]);
-                }
-                key = btoa(bytes);
-            } else {
-                // legacy browser or unsupported environment
-                throw 'No supported crypto API detected, you may read pastes and post comments, but can\'t create pastes.';
-            }
-            return key;
+            return btoa(getRandomBytes(32));
         };
 
         return me;

+ 14 - 7
js/test/CryptTool.js

@@ -10,10 +10,12 @@ describe('CryptTool', function () {
                 'string',
                 'string',
                 function (key, password, message) {
+                    jsdom();
+                    window.crypto = new WebCrypto();
                     return message === $.PrivateBin.CryptTool.decipher(
                         key,
                         password,
-                        $.PrivateBin.CryptTool.cipher(key, password, message)
+                        $.PrivateBin.CryptTool.cipher(key, password, message.trim())
                     );
                 }
             ),
@@ -25,15 +27,16 @@ describe('CryptTool', function () {
         // SJCL based pastes still works
         it(
             'supports PrivateBin v1 ciphertext (SJCL & browser atob)',
-            function () {
+            async function () {
                 delete global.Base64;
                 // make btoa available
                 jsdom();
                 global.btoa = window.btoa;
+                window.crypto = new WebCrypto();
 
                 // Of course you can easily decipher the following texts, if you like.
                 // Bonus points for finding their sources and hidden meanings.
-                var paste1 = $.PrivateBin.CryptTool.decipher(
+                var paste1 = await $.PrivateBin.CryptTool.decipher(
                     '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=',
                     // -- "That's amazing. I've got the same combination on my luggage."
                     Array.apply(0, Array(6)).map(function(_,b) { return b + 1; }).join(''),
@@ -65,7 +68,7 @@ describe('CryptTool', function () {
                     'C5fARPJ4F2BIWgzgzkNj+dVjusft2XnziamWdbS5u3kuRlVuz5LQj+R5' +
                     'imnqQAincdZTkTT1nYx+DatlOLllCYIHffpI="}'
                 ),
-                paste2 = $.PrivateBin.CryptTool.decipher(
+                paste2 = await $.PrivateBin.CryptTool.decipher(
                     's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=',
                     '', // no password
                     '{"iv":"WA42mdxIVXUwBqZu7JYNiw==","v":1,"iter":10000,"ks"' +
@@ -101,12 +104,14 @@ describe('CryptTool', function () {
 
         it(
             'supports ZeroBin ciphertext (SJCL & Base64 1.7)',
-            function () {
+            async function () {
                 global.Base64 = require('../base64-1.7').Base64;
+                jsdom();
+                window.crypto = new WebCrypto();
 
                 // Of course you can easily decipher the following texts, if you like.
                 // Bonus points for finding their sources and hidden meanings.
-                var paste1 = $.PrivateBin.CryptTool.decipher(
+                var paste1 = await $.PrivateBin.CryptTool.decipher(
                     '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=',
                     // -- "That's amazing. I've got the same combination on my luggage."
                     Array.apply(0, Array(6)).map(function(_,b) { return b + 1; }).join(''),
@@ -130,7 +135,7 @@ describe('CryptTool', function () {
                     'V37AeiNoD2PcI6ZcHbRdPa+XRrRcJhSPPW7UQ0z4OvBfjdu/w390QxAx' +
                     'SxvZewoh49fKKB6hTsRnZb4tpHkjlww=="}'
                 ),
-                paste2 = $.PrivateBin.CryptTool.decipher(
+                paste2 = await $.PrivateBin.CryptTool.decipher(
                     's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=',
                     '', // no password
                     '{"iv":"Z7lAZQbkrqGMvruxoSm6Pw==","v":1,"iter":10000,"ks"' +
@@ -167,6 +172,8 @@ describe('CryptTool', function () {
             'returns random, non-empty keys',
             'integer',
             function(counter) {
+                jsdom();
+                window.crypto = new WebCrypto();
                 var key = $.PrivateBin.CryptTool.getSymmetricKey(),
                     result = (key !== '' && keys.indexOf(key) === -1);
                 keys.push(key);

+ 4 - 5
js/test/Helper.js

@@ -213,15 +213,14 @@ describe('Helper', function () {
         this.timeout(30000);
         jsc.property(
             'returns the requested cookie',
-            'nearray asciinestring',
-            'nearray asciistring',
+            jsc.nearray(jsc.nearray(common.jscAlnumString())),
+            jsc.nearray(jsc.nearray(common.jscAlnumString())),
             function (labels, values) {
                 var selectedKey = '', selectedValue = '',
                     cookieArray = [];
                 labels.forEach(function(item, i) {
-                    // deliberatly using a non-ascii key for replacing invalid characters
-                    var key = item.replace(/[\s;,=]/g, Array(i+2).join('£')),
-                        value = (values[i] || values[0]).replace(/[\s;,=]/g, '');
+                    var key = item.join(''),
+                        value = (values[i] || values[0]).join('');
                     cookieArray.push(key + '=' + value);
                     if (Math.random() < 1 / i || selectedKey === key)
                     {

+ 1 - 1
tpl/bootstrap.php

@@ -71,7 +71,7 @@ if ($MARKDOWN):
 endif;
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-1.0.7.js" integrity="sha512-VnKJHLosO8z2ojNvWk9BEKYqnhZyWK9rM90FgZUUEp/PRnUqR5OLLKE0a3BkVmn7YgB7LXRrjHgFHQYKd6DAIA==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-MBMWkPzbqMbXFxOAWrJ50mzsZLDbDHKL+1z8uJ4E6nJ7BpAUYh7KnxV9GxuSKS7ZTguVoatCdG1KJ1MjdSimmQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-O3MbgA0NDp6Lklnj0GfSs7l5UkuuF/C+/rqjhVwr/Qw/b3jNEyBsl0ufQ3ruweBH10M189aquTnrmSifu1hDlA==" crossorigin="anonymous"></script>
 		<!--[if lt IE 10]>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
 		<![endif]-->

+ 1 - 1
tpl/page.php

@@ -49,7 +49,7 @@ if ($MARKDOWN):
 endif;
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-1.0.7.js" integrity="sha512-VnKJHLosO8z2ojNvWk9BEKYqnhZyWK9rM90FgZUUEp/PRnUqR5OLLKE0a3BkVmn7YgB7LXRrjHgFHQYKd6DAIA==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-MBMWkPzbqMbXFxOAWrJ50mzsZLDbDHKL+1z8uJ4E6nJ7BpAUYh7KnxV9GxuSKS7ZTguVoatCdG1KJ1MjdSimmQ==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-O3MbgA0NDp6Lklnj0GfSs7l5UkuuF/C+/rqjhVwr/Qw/b3jNEyBsl0ufQ3ruweBH10M189aquTnrmSifu1hDlA==" crossorigin="anonymous"></script>
 		<!--[if lt IE 10]>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
 		<![endif]-->

+ 6 - 0
tst/README.md

@@ -69,6 +69,12 @@ $ npm install jsverify jsdom@9 jsdom-global@2 mime-types
 Note: If you use a distribution that provides nodeJS >= 6, then you can install
 the latest jsdom and jsdom-global packages and don't need to use @9 and @2.
 
+Note: When running Ubuntu 18.04, there is [a bug](https://bugs.launchpad.net/ubuntu/+source/nodejs/+bug/1779863)
+due to the mismatch of nodejs 8 and OpenSSL 1.1 library it was compiled against.
+Until this is solved, you may have to use [a PPA of nodejs, compiled against
+OpenSSL 1.0](https://launchpad.net/~ddstreet/+archive/ubuntu/lp1779863) or use
+nodejs 10 or later from a different source.
+
 To run the tests, just change into the `js` directory and run istanbul:
 ```console
 $ cd PrivateBin/js