Merge remote-tracking branch 'remotes/thororm/master' into attachment-handling

# Conflicts:
#	tpl/bootstrap.php
#	tpl/page.php
This commit is contained in:
thororm 2017-05-13 19:48:25 +02:00
commit 23f5dfbff8
16 changed files with 468 additions and 80 deletions

1
.gitignore vendored
View File

@ -30,6 +30,7 @@ vendor/**/build_phar.php
# Ignore local node modules, unit testing logs, api docs and eclipse project files # Ignore local node modules, unit testing logs, api docs and eclipse project files
js/node_modules/ js/node_modules/
js/test.log
tst/log/ tst/log/
tst/ConfigurationCombinationsTest.php tst/ConfigurationCombinationsTest.php
.settings .settings

View File

@ -3,7 +3,7 @@
## Active contributors ## Active contributors
Simon Rupf - current developer and maintainer Simon Rupf - current developer and maintainer
rugk - security review, doc improvment & various other stuff rugk - security review, doc improvment, JS refactoring & various other stuff
## Past contributions ## Past contributions

View File

@ -64,7 +64,7 @@ process (see also
> #### PATH Example > #### PATH Example
> Your PrivateBin installation lives in a subfolder called "paste" inside of > Your PrivateBin installation lives in a subfolder called "paste" inside of
> your document root. The URL looks like this: > your document root. The URL looks like this:
> http://example.com/paste/ > https://example.com/paste/
> >
> The full path of PrivateBin on your webserver is: > The full path of PrivateBin on your webserver is:
> /home/example.com/htdocs/paste > /home/example.com/htdocs/paste
@ -118,7 +118,8 @@ For reference or if you want to create the table schema for yourself (replace
`prefix_` with your own table prefix and create the table schema with phpMyAdmin `prefix_` with your own table prefix and create the table schema with phpMyAdmin
or the MYSQL console): or the MYSQL console):
CREATE TABLE prefix_paste ( ```sql
CREATE TABLE prefix_paste (
dataid CHAR(16) NOT NULL, dataid CHAR(16) NOT NULL,
data BLOB, data BLOB,
postdate INT, postdate INT,
@ -129,9 +130,9 @@ or the MYSQL console):
attachment MEDIUMBLOB, attachment MEDIUMBLOB,
attachmentname BLOB, attachmentname BLOB,
PRIMARY KEY (dataid) PRIMARY KEY (dataid)
); );
CREATE TABLE prefix_comment ( CREATE TABLE prefix_comment (
dataid CHAR(16), dataid CHAR(16),
pasteid CHAR(16), pasteid CHAR(16),
parentid CHAR(16), parentid CHAR(16),
@ -140,10 +141,14 @@ or the MYSQL console):
vizhash BLOB, vizhash BLOB,
postdate INT, postdate INT,
PRIMARY KEY (dataid) PRIMARY KEY (dataid)
); );
CREATE INDEX parent ON prefix_comment(pasteid); CREATE INDEX parent ON prefix_comment(pasteid);
CREATE TABLE prefix_config ( CREATE TABLE prefix_config (
id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id) id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id)
); );
INSERT INTO prefix_config VALUES('VERSION', '1.1'); INSERT INTO prefix_config VALUES('VERSION', '1.1');
```
In PostgreSQL the attachment column needs to be TEXT and not BLOB or MEDIUMBLOB.

View File

@ -21,6 +21,10 @@ fileupload = false
; preselect the burn-after-reading feature, defaults to false ; preselect the burn-after-reading feature, defaults to false
burnafterreadingselected = false burnafterreadingselected = false
; delete a burn after reading paste immediatly after it is first accessed from
; the server and do not wait for a successful decryption
instantburnafterreading = false
; which display mode to preselect by default, defaults to "plaintext" ; which display mode to preselect by default, defaults to "plaintext"
; make sure the value exists in [formatter_options] ; make sure the value exists in [formatter_options]
defaultformatter = "plaintext" defaultformatter = "plaintext"

View File

@ -4,7 +4,7 @@
"type": "project", "type": "project",
"keywords": ["private", "secure", "end-to-end-encrypted", "e2e", "paste", "pastebin", "zero", "zero-knowledge", "encryption", "encrypted", "AES"], "keywords": ["private", "secure", "end-to-end-encrypted", "e2e", "paste", "pastebin", "zero", "zero-knowledge", "encryption", "encrypted", "AES"],
"homepage": "https://github.com/PrivateBin", "homepage": "https://github.com/PrivateBin",
"license":"zlib-acknowledgement", "license":"zlib",
"support": { "support": {
"issues": "https://github.com/PrivateBin/PrivateBin/issues", "issues": "https://github.com/PrivateBin/PrivateBin/issues",
"wiki": "https://github.com/PrivateBin/PrivateBin/wiki", "wiki": "https://github.com/PrivateBin/PrivateBin/wiki",

View File

@ -1,7 +1,7 @@
{ {
"PrivateBin": "PrivateBin", "PrivateBin": "PrivateBin",
"%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.": "%s is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href=\"https://privatebin.info/\">project page</a>.":
"%s est un 'pastebin' (ou gestionnaire d'extraits de texte et de code source) minimaliste et open source, dans lequel le serveur n'a aucune connaissance des données envoyées. Les données sont chiffrées/déchiffrées <i>dans le navigateur</i> par un chiffrage AES 256 bits. Plus d'informations sur <a href=\"https://privatebin.info/\">la page du projet</a>.", "%s est un 'pastebin' (ou gestionnaire d'extraits de texte et de code source) minimaliste et open source, dans lequel le serveur n'a aucune connaissance des données envoyées. Les données sont chiffrées/déchiffrées <i>dans le navigateur</i> par un chiffrement AES 256 bits. Plus d'informations sur <a href=\"https://privatebin.info/\">la page du projet</a>.",
"Because ignorance is bliss": "Because ignorance is bliss":
"Parce que l'ignorance c'est le bonheur", "Parce que l'ignorance c'est le bonheur",
"en": "fr", "en": "fr",
@ -123,7 +123,7 @@
"Could not create paste: %s": "Could not create paste: %s":
"Impossible de créer le paste : %s", "Impossible de créer le paste : %s",
"Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
"Impossible de déchiffrer le paste : Clé de déchiffrage manquante dans l'URL (Avez-vous utilisé un redirecteur ou un site de réduction d'URL qui supprime une partie de l'URL ?)", "Impossible de déchiffrer le paste : Clé de déchiffrement manquante dans l'URL (Avez-vous utilisé un redirecteur ou un site de réduction d'URL qui supprime une partie de l'URL ?)",
"B": "o", "B": "o",
"KiB": "Kio", "KiB": "Kio",
"MiB": "Mio", "MiB": "Mio",
@ -156,8 +156,8 @@
"Enter password": "Enter password":
"Entrez le mot de passe", "Entrez le mot de passe",
"Loading…": "Chargement…", "Loading…": "Chargement…",
"Decrypting paste…": "Decrypting paste…", "Decrypting paste…": "Déchiffrement du paste…",
"Preparing new paste…": "Preparing new paste…", "Preparing new paste…": "Préparation du paste…",
"In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.": "In case this message never disappears please have a look at <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">this FAQ for information to troubleshoot</a>.":
"Si ce message ne disparaîssait pas, jetez un oeil à <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">cette FAQ pour des idées de résolution</a> (en Anglais).", "Si ce message ne disparaîssait pas, jetez un oeil à <a href=\"https://github.com/PrivateBin/PrivateBin/wiki/FAQ#why-does-not-the-loading-message-go-away\">cette FAQ pour des idées de résolution</a> (en Anglais).",
"+++ no paste text +++": "+++ no paste text +++" "+++ no paste text +++": "+++ no paste text +++"

View File

@ -107,19 +107,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
return [v, 'month']; return [v, 'month'];
} }
/**
* checks if a string is valid text (and not onyl whitespace)
*
* @name Helper.isValidText
* @function
* @param {string} string
* @return {bool}
*/
me.isValidText = function(string)
{
return (string.length > 0 && $.trim(string) !== '')
}
/** /**
* text range selection * text range selection
* *
@ -319,7 +306,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
* @param {object} document * @param {object} document
* @class * @class
*/ */
var I18n = (function (window, document) { var I18n = (function () {
var me = {}; var me = {};
/** /**
@ -551,8 +538,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
}); });
} }
/**
* resets state, used for unit testing
*
* @name I18n.reset
* @function
*/
me.reset = function(mockLanguage, mockTranslations)
{
language = mockLanguage || null;
translations = mockTranslations || {};
}
return me; return me;
})(window, document); })();
/** /**
* handles everything related to en/decryption * handles everything related to en/decryption
@ -823,7 +822,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
$cipherData = $templates = id = symmetricKey = null; $cipherData = $templates = id = symmetricKey = null;
} }
/** /**
* init navigation manager * init navigation manager
* *

View File

@ -13,15 +13,22 @@ var jsc = require('jsverify'),
}) })
), ),
// schemas supported by the whatwg-url library // schemas supported by the whatwg-url library
schemas = ['ftp','gopher','http','https','ws','wss']; schemas = ['ftp','gopher','http','https','ws','wss'],
supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'],
logFile = require('fs').createWriteStream('test.log');
global.$ = global.jQuery = require('./jquery-3.1.1'); global.$ = global.jQuery = require('./jquery-3.1.1');
global.sjcl = require('./sjcl-1.0.6'); global.sjcl = require('./sjcl-1.0.6');
global.Base64 = require('./base64-2.1.9'); global.Base64 = require('./base64-2.1.9').Base64;
global.RawDeflate = require('./rawdeflate-0.5'); global.RawDeflate = require('./rawdeflate-0.5').RawDeflate;
require('./rawinflate-0.3'); global.RawDeflate.inflate = require('./rawinflate-0.3').RawDeflate.inflate;
require('./privatebin'); require('./privatebin');
// redirect console messages to log file
console.warn = console.error = function (msg) {
logFile.write(msg + '\n');
}
describe('Helper', function () { describe('Helper', function () {
describe('secondsToHuman', function () { describe('secondsToHuman', function () {
after(function () { after(function () {
@ -195,9 +202,10 @@ describe('Helper', function () {
'(small nearray) string', '(small nearray) string',
'string', 'string',
function (prefix, params, postfix) { function (prefix, params, postfix) {
var prefix = prefix.replace(/%(s|d)/g, '%%'), prefix = prefix.replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%'), params[0] = params[0].replace(/%(s|d)/g, '%%');
result = prefix + params[0] + postfix; postfix = postfix.replace(/%(s|d)/g, '%%');
var result = prefix + params[0] + postfix;
params.unshift(prefix + '%s' + postfix); params.unshift(prefix + '%s' + postfix);
return result === $.PrivateBin.Helper.sprintf.apply(this, params); return result === $.PrivateBin.Helper.sprintf.apply(this, params);
} }
@ -208,9 +216,9 @@ describe('Helper', function () {
'(small nearray) nat', '(small nearray) nat',
'string', 'string',
function (prefix, params, postfix) { function (prefix, params, postfix) {
var prefix = prefix.replace(/%(s|d)/g, '%%'), prefix = prefix.replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%'), postfix = postfix.replace(/%(s|d)/g, '%%');
result = prefix + params[0] + postfix; var result = prefix + params[0] + postfix;
params.unshift(prefix + '%d' + postfix); params.unshift(prefix + '%d' + postfix);
return result === $.PrivateBin.Helper.sprintf.apply(this, params); return result === $.PrivateBin.Helper.sprintf.apply(this, params);
} }
@ -221,9 +229,9 @@ describe('Helper', function () {
'(small nearray) falsy', '(small nearray) falsy',
'string', 'string',
function (prefix, params, postfix) { function (prefix, params, postfix) {
var prefix = prefix.replace(/%(s|d)/g, '%%'), prefix = prefix.replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%'), postfix = postfix.replace(/%(s|d)/g, '%%');
result = prefix + '0' + postfix; var result = prefix + '0' + postfix;
params.unshift(prefix + '%d' + postfix); params.unshift(prefix + '%d' + postfix);
return result === $.PrivateBin.Helper.sprintf.apply(this, params) return result === $.PrivateBin.Helper.sprintf.apply(this, params)
} }
@ -236,9 +244,10 @@ describe('Helper', function () {
'string', 'string',
'string', 'string',
function (prefix, uint, middle, string, postfix) { function (prefix, uint, middle, string, postfix) {
var prefix = prefix.replace(/%(s|d)/g, '%%'), prefix = prefix.replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%'), middle = middle.replace(/%(s|d)/g, '%%');
params = [prefix + '%d' + middle + '%s' + postfix, uint, string], postfix = postfix.replace(/%(s|d)/g, '%%');
var params = [prefix + '%d' + middle + '%s' + postfix, uint, string],
result = prefix + uint + middle + string + postfix; result = prefix + uint + middle + string + postfix;
return result === $.PrivateBin.Helper.sprintf.apply(this, params); return result === $.PrivateBin.Helper.sprintf.apply(this, params);
} }
@ -251,9 +260,10 @@ describe('Helper', function () {
'string', 'string',
'string', 'string',
function (prefix, uint, middle, string, postfix) { function (prefix, uint, middle, string, postfix) {
var prefix = prefix.replace(/%(s|d)/g, '%%'), prefix = prefix.replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%'), middle = middle.replace(/%(s|d)/g, '%%');
params = [prefix + '%s' + middle + '%d' + postfix, string, uint], postfix = postfix.replace(/%(s|d)/g, '%%');
var params = [prefix + '%s' + middle + '%d' + postfix, string, uint],
result = prefix + string + middle + uint + postfix; result = prefix + string + middle + uint + postfix;
return result === $.PrivateBin.Helper.sprintf.apply(this, params); return result === $.PrivateBin.Helper.sprintf.apply(this, params);
} }
@ -270,10 +280,11 @@ describe('Helper', function () {
cookieArray = [], cookieArray = [],
count = 0; count = 0;
labels.forEach(function(item, i) { labels.forEach(function(item, i) {
var key = item.replace(/[\s;,=]/g, 'x'), // 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, ''); value = (values[i] || values[0]).replace(/[\s;,=]/g, '');
cookieArray.push(key + '=' + value); cookieArray.push(key + '=' + value);
if (Math.random() < 1 / i) if (Math.random() < 1 / i || selectedKey === key)
{ {
selectedKey = key; selectedKey = key;
selectedValue = value; selectedValue = value;
@ -325,10 +336,235 @@ describe('Helper', function () {
}); });
}); });
describe('I18n', function () {
describe('translate', function () {
before(function () {
$.PrivateBin.I18n.reset();
});
jsc.property(
'returns message ID unchanged if no translation found',
'string',
function (messageId) {
messageId = messageId.replace(/%(s|d)/g, '%%');
var plurals = [messageId, messageId + 's'],
fake = [messageId],
result = $.PrivateBin.I18n.translate(messageId);
$.PrivateBin.I18n.reset();
var alias = $.PrivateBin.I18n._(messageId);
$.PrivateBin.I18n.reset();
var p_result = $.PrivateBin.I18n.translate(plurals);
$.PrivateBin.I18n.reset();
var p_alias = $.PrivateBin.I18n._(plurals);
$.PrivateBin.I18n.reset();
var f_result = $.PrivateBin.I18n.translate(fake);
$.PrivateBin.I18n.reset();
var f_alias = $.PrivateBin.I18n._(fake);
$.PrivateBin.I18n.reset();
return messageId === result && messageId === alias &&
messageId === p_result && messageId === p_alias &&
messageId === f_result && messageId === f_alias;
}
);
jsc.property(
'replaces %s in strings with first given parameter',
'string',
'(small nearray) string',
'string',
function (prefix, params, postfix) {
prefix = prefix.replace(/%(s|d)/g, '%%');
params[0] = params[0].replace(/%(s|d)/g, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%');
var translation = prefix + params[0] + postfix;
params.unshift(prefix + '%s' + postfix);
var result = $.PrivateBin.I18n.translate.apply(this, params);
$.PrivateBin.I18n.reset();
var alias = $.PrivateBin.I18n._.apply(this, params);
$.PrivateBin.I18n.reset();
return translation === result && translation === alias;
}
);
});
describe('getPluralForm', function () {
before(function () {
$.PrivateBin.I18n.reset();
});
jsc.property(
'returns valid key for plural form',
jsc.elements(supportedLanguages),
'integer',
function(language, n) {
$.PrivateBin.I18n.reset(language);
var result = $.PrivateBin.I18n.getPluralForm(n);
// arabic seems to have the highest plural count with 6 forms
return result >= 0 && result <= 5;
}
);
});
// loading of JSON via AJAX needs to be tested in the browser, this just mocks it
// TODO: This needs to be tested using a browser.
describe('loadTranslations', function () {
before(function () {
$.PrivateBin.I18n.reset();
});
jsc.property(
'downloads and handles any supported language',
jsc.elements(supportedLanguages),
function(language) {
var clean = jsdom('', {url: 'https://privatebin.net/', cookie: ['lang=' + language]});
$.PrivateBin.I18n.reset('en');
$.PrivateBin.I18n.loadTranslations();
$.PrivateBin.I18n.reset(language, require('../i18n/' + language + '.json'));
var result = $.PrivateBin.I18n.translate('en'),
alias = $.PrivateBin.I18n._('en');
clean();
return language === result && language === alias;
}
);
});
});
describe('CryptTool', function () {
describe('cipher & decipher', function () {
this.timeout(30000);
it('can en- and decrypt any message', function () {
jsc.check(jsc.forall(
'string',
'string',
'string',
function (key, password, message) {
return message === $.PrivateBin.CryptTool.decipher(
key,
password,
$.PrivateBin.CryptTool.cipher(key, password, message)
);
}
),
// reducing amount of checks as running 100 takes about 5 minutes
{tests: 5, quiet: true});
});
// The below static unit tests are included to ensure deciphering of "classic"
// SJCL based pastes still works
it(
'supports PrivateBin v1 ciphertext (SJCL & Base64 2.1.9)',
function () {
// 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(
'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(''),
'{"iv":"4HNFIl7eYbCh6HuShctTIA==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"u0lQvePq6L0=","ct":"fGPUVrDyaVr1ZDGb+kqQ3CPEW8x4YKGfzHDmA0Vjkh250aWNe7Cnigkps9aaFVMX9AaerrTp3yZbojJtNqVGMfLdUTu+53xmZHqRKxCCqSfDNSNoW4Oxk5OVgAtRyuG4bXHDsWTXDNz2xceqzVFqhkwTwlUchrV7uuFK/XUKTNjPFM744moivIcBbfM2FOeKlIFs8RYPYuvqQhp2rMLlNGwwKh//4kykQsHMQDeSDuJl8stMQzgWR/btUBZuwNZEydkMH6IPpTdf5WTSrZ+wC2OK0GutCm4UaEe6txzaTMfu+WRVu4PN6q+N+2zljWJ1XdpVcN/i0Sv4QVMym0Xa6y0eccEhj/69o47PmExmMMeEwExImPalMNT9JUSiZdOZJ/GdzwrwoIuq1mdQR6vSH+XJ/8jXJQ7bjjJVJYXTcT0Di5jixArI2Kpp1GGlGVFbLgPugwU1wczg+byqeDOAECXRRnQcogeaJtVcRwXwfy4j3ORFcblYMilxyHqKBewcYPRVBGtBs50cVjSIkAfR84rnc1nfvnxK/Gmm+4VBNHI6ODWNpRolVMCzXjbKYnV3Are5AgSpsTqaGl41VJGpcco6cAwi4K0Bys1seKR+bLSdUgqRrkEqSRSdu3/VTu9HhEk8an0rjTE4CBB5/LMn16p0TGLoOb32odKFIEtpanVvLjeyiVMvSxcgYLNnTi/5FiaAC4pJxRD+AZHedU1FICUeEXxIcac/4E5qjkHjX9SpQtLl80QLIVnjNliZm7QLB/nKu7W8Jb0+/CiTdV3Q9LhxlH4ciprnX+W0B00BKYFHnL9jRVzKdXhf1EHydbXMAfpCjHAXIVCkFakJinQBDIIw/SC6Yig0u0ddEID2B7LYAP1iE4RZwzTrxCB+ke2jQr8c20Jj6u6ShFOPC9DCw9XupZ4HAalVG00kSgjus+b8zrVji3/LKEhb4EBzp1ctBJCFTeXwej8ZETLoXTylev5dlwZSYAbuBPPcbFR/xAIPx3uDabd1E1gTqUc68ICIGhd197Mb2eRWiSvHr5SPsASerMxId6XA6+iQlRiI+NDR+TGVNmCnfxSlyPFMOHGTmslXOGIqGfBR8l4ft8YVZ70lCwmwTuViGc75ULSf9mM57/LmRzQFMYQtvI8IFK9JaQEMY5xz0HLtR4iyQUUdwR9e0ytBNdWF2a2WPDEnJuY/QJo4GzTlgv4QUxMXI5htsn2rf0HxCFu7Po8DNYLxTS+67hYjDIYWYaEIc8LXWMLyDm9C5fARPJ4F2BIWgzgzkNj+dVjusft2XnziamWdbS5u3kuRlVuz5LQj+R5imnqQAincdZTkTT1nYx+DatlOLllCYIHffpI="}'
),
paste2 = $.PrivateBin.CryptTool.decipher(
's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=',
'', // no password
'{"iv":"WA42mdxIVXUwBqZu7JYNiw==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"jN6CjbQMJCM=","ct":"kYYMo5DFG1+w0UHiYXT5pdV0IUuXxzOlslkW/c3DRCbGFROCVkAskHce7HoRczee1N9c5MhHjVMJUIZE02qIS8UyHdJ/GqcPVidTUcj9rnDNWsTXkjVv8jCwHS/cwmAjDTWpwp5ThECN+ov/wNp/NdtTj8Qj7f/T3rfZIOCWfwLH9s4Des35UNcUidfPTNQ1l0Gm0X+r98CCUSYZjQxkZc6hRZBLPQ8EaNVooUwd5eP4GiYlmSDNA0wOSA+5isPYxomVCt+kFf58VBlNhpfNi7BLYAUTPpXT4SfH5drR9+C7NTeZ+tTCYjbU94PzYItOpu8vgnB1/a6BAM5h3m9w+giUb0df4hgTWeZnZxLjo5BN8WV+kdTXMj3/Vv0gw0DQrDcCuX/cBAjpy3lQGwlAN1vXoOIyZJUjMpQRrOLdKvLB+zcmVNtGDbgnfP2IYBzk9NtodpUa27ne0T0ZpwOPlVwevsIVZO224WLa+iQmmHOWDFFpVDlS0t0fLfOk7Hcb2xFsTxiCIiyKMho/IME1Du3X4e6BVa3hobSSZv0rRtNgY1KcyYPrUPW2fxZ+oik3y9SgGvb7XpjVIta8DWlDWRfZ9kzoweWEYqz9IA8Xd373RefpyuWI25zlHoX3nwljzsZU6dC//h/Dt2DNr+IAvKO3+u23cWoB9kgcZJ2FJuqjLvVfCF+OWcig7zs2pTYJW6Rg6lqbBCxiUUlae6xJrjfv0pzD2VYCLY7v1bVTagppwKzNI3WaluCOrdDYUCxUSe56yd1oAoLPRVbYvomRboUO6cjQhEknERyvt45og2kORJOEJayHW+jZgR0Y0jM3Nk17ubpij2gHxNx9kiLDOiCGSV5mn9mV7qd3HHcOMSykiBgbyzjobi96LT2dIGLeDXTIdPOog8wyobO4jWq0GGs0vBB8oSYXhHvixZLcSjX2KQuHmEoWzmJcr3DavdoXZmAurGWLKjzEdJc5dSD/eNr99gjHX7wphJ6umKMM+fn6PcbYJkhDh2GlJL5COXjXfm/5aj/vuyaRRWZMZtmnYpGAtAPg7AUG"}'
);
if (!paste1.includes('securely packed in iron') || !paste2.includes('Sol is right')) {
throw Error('v1 (SJCL based) pastes could not be deciphered');
}
}
);
it(
'supports ZeroBin ciphertext (SJCL & Base64 1.7)',
function () {
var newBase64 = global.Base64;
global.Base64 = require('./base64-1.7').Base64;
jsdom();
delete require.cache[require.resolve('./privatebin')];
require('./privatebin');
// 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(
'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(''),
'{"iv":"aTnR2qBL1CAmLX8FdWe3VA==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"u0lQvePq6L0=","ct":"A3nBTvICZtYy6xqbIJE0c8Veored5lMJUGgGUm4581wjrPFlU0Q0tUZSf+RUUoZj2jqDa4kiyyZ5YNMe30hNMV0oVSalNhRgD9svVMnPuF162IbyhVCwr7ULjT981CHxVlGNqGqmIU6L/XixgdArxAA8x1GCrfAkBWWGeq8Qw5vJPG/RCHpwR4Wy3azrluqeyERBzmaOQjO/kM35TiI6IrLYFyYyL7upYlxAaxS0XBMZvN8QU8Lnerwvh5JVC6OkkKrhogajTJIKozCF79yI78c50LUh7tTuI3Yoh7+fXxhoODvQdYFmoiUlrutN7Y5ZMRdITvVu8fTYtX9c7Fiufmcq5icEimiHp2g1bvfpOaGOsFT+XNFgC9215jcp5mpBdN852xs7bUtw+nDrf+LsDEX6iRpRZ+PYgLDN5xQT1ByEtYbeP+tO38pnx72oZdIB3cj8UkOxnxdNiZM5YB5egn4jUj1fHot1I69WoTiUJipZ5PIATv7ScymRB+AYzjxjurQ9lVfX9QtAbEH2dhdmoUo3IDRSXpWNCe9RC1aUIyWfZO7oI7FEohNscHNTLEcT+wFnFUPByLlXmjNZ7FKeNpvUm3jTY4t4sbZH8o2dUl624PAw1INcJ6FKqWGWwoFT2j1MYC+YV/LkLTdjuWfayvwLMh27G/FfKCRbW36vqinegqpPDylsx9+3oFkEw3y5Z8+44oN91rE/4Md7JhPJeRVlFC9TNCj4dA+EVhbbQqscvSnIH2uHkMw7mNNo7xba/YT9KoPDaniqnYqb+q2pX1WNWE7dLS2wfroMAS3kh8P22DAV37AeiNoD2PcI6ZcHbRdPa+XRrRcJhSPPW7UQ0z4OvBfjdu/w390QxAxSxvZewoh49fKKB6hTsRnZb4tpHkjlww=="}'
),
paste2 = $.PrivateBin.CryptTool.decipher(
's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=',
'', // no password
'{"iv":"Z7lAZQbkrqGMvruxoSm6Pw==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"jN6CjbQMJCM=","ct":"PuOPWB3i2FPcreSrLYeQf84LdE8RHjsc+MGtiOr4b7doNyWKYtkNorbRadxaPnEee2/Utrp1MIIfY5juJSy8RGwEPX5ciWcYe6EzsXWznsnvhmpKNj9B7eIIrfSbxfy8E2e/g7xav1nive+ljToka3WT1DZ8ILQd/NbnJeHWaoSEOfvz8+d8QJPb1tNZvs7zEY95DumQwbyOsIMKAvcZHJ9OJNpujXzdMyt6DpcFcqlldWBZ/8q5rAUTw0HNx/rCgbhAxRYfNoTLIcMM4L0cXbPSgCjwf5FuO3EdE13mgEDhcClW79m0QvcnIh8xgzYoxLbp0+AwvC/MbZM8savN/0ieWr2EKkZ04ggiOIEyvfCUuNprQBYO+y8kKduNEN6by0Yf4LRCPfmwN+GezDLuzTnZIMhPbGqUAdgV6ExqK2ULEEIrQEMoOuQIxfoMhqLlzG79vXGt2O+BY+4IiYfvmuRLks4UXfyHqxPXTJg48IYbGs0j4TtJPUgp3523EyYLwEGyVTAuWhYAmVIwd/hoV7d7tmfcF73w9dufDFI3LNca2KxzBnWNPYvIZKBwWbq8ncxkb191dP6mjEi7NnhqVk5A6vIBbu4AC5PZf76l6yep4xsoy/QtdDxCMocCXeAML9MQ9uPQbuspOKrBvMfN5igA1kBqasnxI472KBNXsdZnaDddSVUuvhTcETM="}'
);
global.Base64 = newBase64;
jsdom();
delete require.cache[require.resolve('./privatebin')];
require('./privatebin');
if (!paste1.includes('securely packed in iron') || !paste2.includes('Sol is right')) {
throw Error('v1 (SJCL based) pastes could not be deciphered');
}
}
);
});
describe('isEntropyReady & addEntropySeedListener', function () {
it(
'lets us know that enough entropy is collected or make us wait for it',
function(done) {
if ($.PrivateBin.CryptTool.isEntropyReady()) {
done();
} else {
$.PrivateBin.CryptTool.addEntropySeedListener(function() {
done();
});
}
}
);
});
describe('getSymmetricKey', function () {
var keys = [];
// the parameter is used to ensure the test is run more then one time
jsc.property(
'returns random, non-empty keys',
'nat',
function(n) {
var key = $.PrivateBin.CryptTool.getSymmetricKey(),
result = (key !== '' && keys.indexOf(key) === -1);
keys.push(key);
return result;
}
);
});
describe('Base64.js vs SJCL.js vs abab.js', function () {
jsc.property(
'these all return the same base64 string',
'string',
function(string) {
var base64 = Base64.toBase64(string),
sjcl = global.sjcl.codec.base64.fromBits(global.sjcl.codec.utf8String.toBits(string)),
abab = window.btoa(Base64.utob(string));
return base64 === sjcl && sjcl === abab;
}
);
});
});
describe('Model', function () { describe('Model', function () {
describe('getPasteId', function () { describe('getPasteId', function () {
before(function () { before(function () {
$.PrivateBin.Model.reset(); $.PrivateBin.Model.reset();
cleanup();
}); });
jsc.property( jsc.property(
@ -349,6 +585,28 @@ describe('Model', function () {
return queryString === result; return queryString === result;
} }
); );
jsc.property(
'throws exception on empty query string',
jsc.nearray(jsc.elements(a2zString)),
jsc.nearray(jsc.elements(a2zString)),
'string',
function (schema, address, fragment) {
var clean = jsdom('', {
url: schema.join('') + '://' + address.join('') +
'/#' + fragment
}),
result = false;
try {
$.PrivateBin.Model.getPasteId();
}
catch(err) {
result = true;
}
$.PrivateBin.Model.reset();
clean();
return result;
}
);
}); });
describe('getPasteKey', function () { describe('getPasteKey', function () {
@ -389,5 +647,27 @@ describe('Model', function () {
return fragmentString === result; return fragmentString === result;
} }
); );
jsc.property(
'throws exception on empty fragment of the URL',
jsc.nearray(jsc.elements(a2zString)),
jsc.nearray(jsc.elements(a2zString)),
jsc.array(jsc.elements(queryString)),
function (schema, address, query) {
var clean = jsdom('', {
url: schema.join('') + '://' + address.join('') +
'/?' + query.join('')
}),
result = false;
try {
$.PrivateBin.Model.getPasteKey();
}
catch(err) {
result = true;
}
$.PrivateBin.Model.reset();
clean();
return result;
}
);
}); });
}); });

View File

@ -42,6 +42,7 @@ class Configuration
'password' => true, 'password' => true,
'fileupload' => false, 'fileupload' => false,
'burnafterreadingselected' => false, 'burnafterreadingselected' => false,
'instantburnafterreading' => false,
'defaultformatter' => 'plaintext', 'defaultformatter' => 'plaintext',
'syntaxhighlightingtheme' => null, 'syntaxhighlightingtheme' => null,
'sizelimit' => 2097152, 'sizelimit' => 2097152,

View File

@ -48,6 +48,11 @@ class Paste extends AbstractModel
$data->meta->remaining_time = $data->meta->expire_date - time(); $data->meta->remaining_time = $data->meta->expire_date - time();
} }
// check if non-expired burn after reading paste needs to be deleted
if (property_exists($data->meta, 'burnafterreading') && $data->meta->burnafterreading && $this->_conf->getKey('instantburnafterreading')) {
$this->delete();
}
// set formatter for for the view. // set formatter for for the view.
if (!property_exists($data->meta, 'formatter')) { if (!property_exists($data->meta, 'formatter')) {
// support < 0.21 syntax highlighting // support < 0.21 syntax highlighting

View File

@ -69,7 +69,7 @@ if ($MARKDOWN):
<?php <?php
endif; endif;
?> ?>
<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Z882259kw/AnperJhiaZzvb7Ic0igEIYCyy/vJYsTu5PqAPRkJzCNzPCSMP37jZkCi7HFbjt5Zl/Vd4w9lHhYA==" crossorigin="anonymous"></script> <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-ofGEJHbk5slAxfzMIcULyZI7a4XcqdyTnATIZPbYv+Ngr+EEt7GZ2QXE8bjt1/Q9EuwQrjsMhiPeGlbaAGqFgA==" crossorigin="anonymous"></script>
<!--[if lt IE 10]> <!--[if lt IE 10]>
<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style> <style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
<![endif]--> <![endif]-->

View File

@ -47,7 +47,7 @@ if ($MARKDOWN):
<?php <?php
endif; endif;
?> ?>
<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Z882259kw/AnperJhiaZzvb7Ic0igEIYCyy/vJYsTu5PqAPRkJzCNzPCSMP37jZkCi7HFbjt5Zl/Vd4w9lHhYA==" crossorigin="anonymous"></script> <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-ofGEJHbk5slAxfzMIcULyZI7a4XcqdyTnATIZPbYv+Ngr+EEt7GZ2QXE8bjt1/Q9EuwQrjsMhiPeGlbaAGqFgA==" crossorigin="anonymous"></script>
<!--[if lt IE 10]> <!--[if lt IE 10]>
<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style> <style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
<![endif]--> <![endif]-->

View File

@ -822,6 +822,37 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
$content, $content,
'outputs data correctly' 'outputs data correctly'
); );
// by default it will be deleted after encryption by the JS
$this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after reading');
}
/**
* @runInSeparateProcess
*/
public function testReadInstantBurn()
{
$this->reset();
$options = parse_ini_file(CONF, true);
$options['main']['instantburnafterreading'] = 1;
Helper::confBackup();
Helper::createIniFile(CONF, $options);
$burnPaste = Helper::getPaste(array('burnafterreading' => true));
$this->_model->create(Helper::getPasteId(), $burnPaste);
$_SERVER['QUERY_STRING'] = Helper::getPasteId();
ob_start();
new PrivateBin;
$content = ob_get_contents();
ob_end_clean();
unset($burnPaste['meta']['salt']);
$this->assertRegExp(
'#<div id="cipherdata"[^>]*>' .
preg_quote(htmlspecialchars(Helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES)) .
'</div>#',
$content,
'outputs data correctly'
);
// in this case the changed configuration deletes it instantly
$this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste exists after reading');
} }
/** /**

View File

@ -2,7 +2,7 @@ Running PHP unit tests
====================== ======================
In order to run these tests, you will need to install the following packages In order to run these tests, you will need to install the following packages
and its dependencies: and their dependencies:
* phpunit * phpunit
* php-gd * php-gd
* php-sqlite3 * php-sqlite3
@ -10,15 +10,33 @@ and its dependencies:
Example for Debian and Ubuntu: Example for Debian and Ubuntu:
```console ```console
$ sudo apt install phpunit php-gd php-sqlite php-xdebug $ sudo apt install phpunit php-gd php-sqlite3 php-xdebug
``` ```
To run the tests, just change into this directory and run phpunit: To run the tests, change into the `tst` directory and run phpunit:
```console ```console
$ cd PrivateBin/tst $ cd PrivateBin/tst
$ phpunit $ phpunit
``` ```
Additionally there is the `ConfigurationTestGenerator`. Based on the
configurations defined in its constructor, it generates the unit test file
`tst/ConfigurationCombinationsTest.php`, containing all possible combinations
of these configurations and tests for (most of the) valid combinations. Some of
combinations can't be tested with this method, i.e. a valid option combined with
an invalid one. Other very specific test cases (i.e. to trigger multiple errors)
are covered in `tst/PrivateBinTest.php`. Here is how to generate the
configuration test and run it:
```console
$ cd PrivateBin/tst
$ php ConfigurationTestGenerator.php
$ phpunit ConfigurationCombinationsTest.php
```
Note that it can take an hour or longer to run the several thousand tests.
Running JavaScript unit tests Running JavaScript unit tests
============================= =============================
@ -36,8 +54,8 @@ $ cd PrivateBin/js
$ npm install jsverify jsdom jsdom-global $ npm install jsverify jsdom jsdom-global
``` ```
Example for Debian and Ubuntu, including steps to allow current user to install Example for Debian and Ubuntu, including steps to allow the current user to
node modules globally: install node modules globally:
```console ```console
$ sudo apt install npm $ sudo apt install npm
$ sudo mkdir /usr/local/lib/node_modules $ sudo mkdir /usr/local/lib/node_modules
@ -54,3 +72,46 @@ $ cd PrivateBin/js
$ istanbul cover _mocha $ istanbul cover _mocha
``` ```
Property based unit testing
---------------------------
In the JavaScript unit tests we use the JSVerify library to leverage property
based unit testing. Instead of artificially creating specific test cases to
cover all relevant paths of the tested code (with the generated coverage reports
providing means to check the tested paths), property based testing allows us to
describe the patterns of data that are valid input.
With each run of the tests, for each `jsc.property` 100 random inputs are
generated and tested. For example we tell the test to generate random strings,
which will include empty strings, numeric strings, long strings, unicode
sequences, etc. This is great for finding corner cases that one might not think
of when explicitly writing one test case at a time.
There is another benefit, too: When an error is found, JSVerify will try to find
the smallest, still failing test case for you and print this out including the
associated random number generator (RNG) state, so you can reproduce it easily:
```console
[...]
30 passing (3s)
1 failing
1) Helper getCookie returns the requested cookie:
Error: Failed after 30 tests and 11 shrinks. rngState: 88caf85079d32e416b; Counterexample: ["{", "9", "9", "YD8%fT"]; [" ", "_|K:"];
[...]
```
Of course it may just be that you need to adjust a test case if the random
pattern generated is ambiguous. In the above example the cookie string would
contain two identical keys "9", something that may not be valid, but that our
code could encounter and needs to be able to handle.
After you adjusted the code of the library or the test you can rerun the test
with the same RNG state as follows:
```console
$ istanbul cover _mocha -- test.js --jsverifyRngState 88caf85079d32e416b
```

View File

@ -23,6 +23,7 @@ return array(
'PrivateBin\\Model\\Comment' => $baseDir . '/lib/Model/Comment.php', 'PrivateBin\\Model\\Comment' => $baseDir . '/lib/Model/Comment.php',
'PrivateBin\\Model\\Paste' => $baseDir . '/lib/Model/Paste.php', 'PrivateBin\\Model\\Paste' => $baseDir . '/lib/Model/Paste.php',
'PrivateBin\\Persistence\\AbstractPersistence' => $baseDir . '/lib/Persistence/AbstractPersistence.php', 'PrivateBin\\Persistence\\AbstractPersistence' => $baseDir . '/lib/Persistence/AbstractPersistence.php',
'PrivateBin\\Persistence\\DataStore' => $baseDir . '/lib/Persistence/DataStore.php',
'PrivateBin\\Persistence\\PurgeLimiter' => $baseDir . '/lib/Persistence/PurgeLimiter.php', 'PrivateBin\\Persistence\\PurgeLimiter' => $baseDir . '/lib/Persistence/PurgeLimiter.php',
'PrivateBin\\Persistence\\ServerSalt' => $baseDir . '/lib/Persistence/ServerSalt.php', 'PrivateBin\\Persistence\\ServerSalt' => $baseDir . '/lib/Persistence/ServerSalt.php',
'PrivateBin\\Persistence\\TrafficLimiter' => $baseDir . '/lib/Persistence/TrafficLimiter.php', 'PrivateBin\\Persistence\\TrafficLimiter' => $baseDir . '/lib/Persistence/TrafficLimiter.php',

View File

@ -52,6 +52,7 @@ class ComposerStaticInitDontChange
'PrivateBin\\Model\\Comment' => __DIR__ . '/../..' . '/lib/Model/Comment.php', 'PrivateBin\\Model\\Comment' => __DIR__ . '/../..' . '/lib/Model/Comment.php',
'PrivateBin\\Model\\Paste' => __DIR__ . '/../..' . '/lib/Model/Paste.php', 'PrivateBin\\Model\\Paste' => __DIR__ . '/../..' . '/lib/Model/Paste.php',
'PrivateBin\\Persistence\\AbstractPersistence' => __DIR__ . '/../..' . '/lib/Persistence/AbstractPersistence.php', 'PrivateBin\\Persistence\\AbstractPersistence' => __DIR__ . '/../..' . '/lib/Persistence/AbstractPersistence.php',
'PrivateBin\\Persistence\\DataStore' => __DIR__ . '/../..' . '/lib/Persistence/DataStore.php',
'PrivateBin\\Persistence\\PurgeLimiter' => __DIR__ . '/../..' . '/lib/Persistence/PurgeLimiter.php', 'PrivateBin\\Persistence\\PurgeLimiter' => __DIR__ . '/../..' . '/lib/Persistence/PurgeLimiter.php',
'PrivateBin\\Persistence\\ServerSalt' => __DIR__ . '/../..' . '/lib/Persistence/ServerSalt.php', 'PrivateBin\\Persistence\\ServerSalt' => __DIR__ . '/../..' . '/lib/Persistence/ServerSalt.php',
'PrivateBin\\Persistence\\TrafficLimiter' => __DIR__ . '/../..' . '/lib/Persistence/TrafficLimiter.php', 'PrivateBin\\Persistence\\TrafficLimiter' => __DIR__ . '/../..' . '/lib/Persistence/TrafficLimiter.php',