diff --git a/.travis.yml b/.travis.yml
index d368a0c..0584c72 100644
--- a/.travis.yml
+++ b/.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
diff --git a/composer.json b/composer.json
index 7f0bd45..2beddab 100644
--- a/composer.json
+++ b/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",
diff --git a/js/common.js b/js/common.js
index 6fc92d4..6aa43ba 100644
--- a/js/common.js
+++ b/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');
diff --git a/js/package.json b/js/package.json
new file mode 100644
index 0000000..7993fbc
--- /dev/null
+++ b/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/"
+}
diff --git a/js/privatebin.js b/js/privatebin.js
index 5e03dd9..05f8ec0 100644
--- a/js/privatebin.js
+++ b/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)
+ me.cipher = async function(key, password, message)
{
- // Galois Counter Mode, keysize 256 bit, authentication tag 128 bit
- var options = {
- mode: 'gcm',
- ks: 256,
- ts: 128
- };
+ // 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) {
- return sjcl.encrypt(key, compress(message), options);
+ 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;
diff --git a/js/test/CryptTool.js b/js/test/CryptTool.js
index 6666921..756c0e9 100644
--- a/js/test/CryptTool.js
+++ b/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);
diff --git a/js/test/Helper.js b/js/test/Helper.js
index f76bf4f..056268b 100644
--- a/js/test/Helper.js
+++ b/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)
{
diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php
index cab961e..4c14115 100644
--- a/tpl/bootstrap.php
+++ b/tpl/bootstrap.php
@@ -71,7 +71,7 @@ if ($MARKDOWN):
endif;
?>
-
+
diff --git a/tpl/page.php b/tpl/page.php
index 411de9d..fdce1ec 100644
--- a/tpl/page.php
+++ b/tpl/page.php
@@ -49,7 +49,7 @@ if ($MARKDOWN):
endif;
?>
-
+
diff --git a/tst/README.md b/tst/README.md
index d7f4ed0..ec42433 100644
--- a/tst/README.md
+++ b/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