Commit 7381d86e authored by Benjamin Bouvier's avatar Benjamin Bouvier Committed by Nicolas Frandeboeuf

Make the salt value facultative and disallow encrypted backups if it's not provided;

parent 9cc151b5
Pipeline #111830 passed with stages
in 6 minutes and 15 seconds
......@@ -7,7 +7,8 @@ import { actions, get } from '../../../store';
const Export = connect(
state => {
return {
isExporting: get.isExporting(state)
isExporting: get.isExporting(state),
canEncrypt: get.boolSetting(state, 'can-encrypt')
};
},
dispatch => {
......@@ -84,14 +85,13 @@ const Export = connect(
maybeSpinner = null;
}
let maybePasswordError = this.state.withPassword ? (
<span>{this.state.passwordError}</span>
) : null;
let submitDisabled =
this.props.isExporting || (this.state.withPassword && !this.state.validPassword);
let maybePasswordForm;
if (this.props.canEncrypt) {
let maybePasswordError = this.state.withPassword ? (
<span>{this.state.passwordError}</span>
) : null;
return (
<div>
maybePasswordForm = (
<div className="backup-password-form">
<label htmlFor="encrypt_with_password">
<input
......@@ -110,6 +110,17 @@ const Export = connect(
/>
{maybePasswordError}
</div>
);
} else {
maybePasswordForm = null;
}
let submitDisabled =
this.props.isExporting || (this.state.withPassword && !this.state.validPassword);
return (
<div>
{maybePasswordForm}
<button
type="button"
id="exportInstance"
......
......@@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
// Global variables
import { actions } from '../../../store';
import { get, actions } from '../../../store';
import { translate as $t } from '../../../helpers';
class ImportModule extends React.Component {
......@@ -90,24 +90,28 @@ class ImportModule extends React.Component {
render() {
let disableButton = this.state.withPassword && !this.state.validPassword;
return (
<div>
<div className="backup-password-form">
<label htmlFor="decrypt_with_password">
<input
id="decrypt_with_password"
type="checkbox"
onChange={this.handleToggleWithPassword}
/>
<span>{$t('client.settings.decrypt_with_password')}</span>
</label>
let maybePasswordForm = this.props.canEncrypt ? (
<div className="backup-password-form">
<label htmlFor="decrypt_with_password">
<input
type="password"
ref={this.refPassword}
disabled={!this.state.withPassword}
onChange={this.handleChangePassword}
id="decrypt_with_password"
type="checkbox"
onChange={this.handleToggleWithPassword}
/>
</div>
<span>{$t('client.settings.decrypt_with_password')}</span>
</label>
<input
type="password"
ref={this.refPassword}
disabled={!this.state.withPassword}
onChange={this.handleChangePassword}
/>
</div>
) : null;
return (
<div>
{maybePasswordForm}
<label
className="btn primary"
......@@ -129,7 +133,11 @@ class ImportModule extends React.Component {
}
const Export = connect(
null,
state => {
return {
canEncrypt: get.boolSetting(state, 'can-encrypt')
};
},
dispatch => {
return {
importInstanceWithoutPassword(data) {
......
......@@ -51,9 +51,7 @@ url_prefix=
; A salt value used in encryption algorithms (used for instance to
; encrypt/decrypt exports). It should be a random string value with
; at least 16 characters that you must provide, otherwise Kresus will
; refuse to start.
; Can be removed; defaults to "".
; at least 16 characters if you decide to provide it.
; Overriden by the KRESUS_SALT environment variable, if it's set.
; Example:
; salt=gj4J89fkjf4h29aDi0f{}fu4389sejk`9osk`
......
......@@ -91,18 +91,17 @@ let OPTIONS = [
{
envName: 'KRESUS_SALT',
configPath: 'config.kresus.salt',
defaultVal: '',
defaultVal: null,
processPath: 'salt',
cleanupAction: val => {
if (!val || val.length < 16) {
if (val !== null && val.length < 16) {
throw new Error('Please provide a salt value with at least 16 characters.');
}
return val;
},
doc: `A salt value used in encryption algorithms (used for instance to
encrypt/decrypt exports). It should be a random string value with
at least 16 characters that you must provide, otherwise Kresus will
refuse to start.`,
at least 16 characters if you decide to provide it.`,
docExample: 'gj4J89fkjf4h29aDi0f{}fu4389sejk`9osk`'
},
......
......@@ -15,8 +15,8 @@ import { validatePassword } from '../../shared/helpers';
import DefaultSettings from '../../shared/default-settings';
import {
assert,
makeLogger,
promisify,
KError,
asyncErr,
getErrorCode,
......@@ -70,13 +70,13 @@ export async function all(req, res) {
const ENCRYPTION_ALGORITHM = 'aes-256-ctr';
const ENCRYPTED_CONTENT_TAG = Buffer.from('KRE');
const pseudoRandomBytes = promisify(crypto.pseudoRandomBytes);
const pbkdf2 = promisify(crypto.pbkdf2);
function encryptData(data, passphrase) {
assert(process.kresus.salt !== null, 'must have provided a salt');
async function encryptData(data, passphrase) {
let initVector = await pseudoRandomBytes(16);
let key = await pbkdf2(passphrase, process.kresus.salt, 100000, 32, 'sha512');
let initVector = crypto.randomBytes(16);
let key = crypto.pbkdf2Sync(passphrase, process.kresus.salt, 100000, 32, 'sha512');
let cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, initVector);
return Buffer.concat([
initVector,
ENCRYPTED_CONTENT_TAG,
......@@ -85,7 +85,9 @@ async function encryptData(data, passphrase) {
]).toString('base64');
}
async function decryptData(data, passphrase) {
function decryptData(data, passphrase) {
assert(process.kresus.salt !== null, 'must have provided a salt');
let rawData = Buffer.from(data, 'base64');
let [initVector, tag, encrypted] = [
rawData.slice(0, 16),
......@@ -101,7 +103,7 @@ async function decryptData(data, passphrase) {
);
}
let key = await pbkdf2(passphrase, process.kresus.salt, 100000, 32, 'sha512');
let key = crypto.pbkdf2Sync(passphrase, process.kresus.salt, 100000, 32, 'sha512');
let decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, key, initVector);
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
......@@ -117,6 +119,13 @@ export async function export_(req, res) {
throw new KError('missing parameter "passphrase"', 400);
}
if (process.kresus.salt === null) {
throw new KError(
"server hasn't been configured for encryption; " +
'please ask your administrator to provide a salt'
);
}
passphrase = req.body.passphrase;
// Check password strength
......@@ -130,7 +139,7 @@ export async function export_(req, res) {
let ret = {};
if (passphrase) {
data = await encryptData(data, passphrase);
data = encryptData(data, passphrase);
ret = {
encrypted: true,
data
......@@ -166,7 +175,14 @@ export async function import_(req, res) {
throw new KError('missing parameter "passphrase"', 400);
}
world = await decryptData(world, req.body.passphrase);
if (process.kresus.salt === null) {
throw new KError(
"server hasn't been configured for encryption; " +
'please ask your administrator to provide a salt'
);
}
world = decryptData(world, req.body.passphrase);
try {
world = JSON.parse(world);
......
......@@ -161,6 +161,12 @@ Setting.all = async function(userId) {
value: String(isEmailEnabled())
});
// Is encryption enabled on the server?
values.push({
name: 'can-encrypt',
value: String(process.kresus.salt !== null)
});
return values;
};
......
......@@ -15,7 +15,8 @@ export const ConfigGhostSettings = new Set([
'weboob-installed',
'standalone-mode',
'url-prefix',
'emails-enabled'
'emails-enabled',
'can-encrypt'
]);
// ACCOUNT TYPES,
......
......@@ -5,6 +5,7 @@ DefaultSettings.set('weboob-installed', 'false');
DefaultSettings.set('weboob-version', null);
DefaultSettings.set('url-prefix', '/');
DefaultSettings.set('emails-enabled', 'false');
DefaultSettings.set('can-encrypt', 'false');
// User settings.
DefaultSettings.set('locale', 'en');
......
......@@ -263,7 +263,7 @@
"export_instance": "Export Kresus instance",
"go_export_instance": "Export",
"exporting": "Exporting…",
"export_instance_help": "This will export all of Kresus data into a format another instance of Kresus can then import back. You can choose to export to a clear-text JSON file (which will not contain bank credentials) or using a password, into an encrypted text file (which will contain bank credentials). If you choose to encrypt your export, the chosen password must contain at least 8 characters, including 1 uppercase letter, 1 lowercase letter and 1 digit.",
"export_instance_help": "This will export all of Kresus data into a format another instance of Kresus can then import back. You can export to a clear-text JSON file (which will not contain bank credentials). If your instance administrator has enabled it, you can also export a backup encrypted with a password (which will contain bank credentials); in this case, the chosen password must contain at least 8 characters, including 1 uppercase letter, 1 lowercase letter and 1 digit.",
"encrypt_with_password": "Encrypt with password",
"weak_password": "Password too weak!",
"error_decrypt_non_encrypted": "You're trying to decrypt an import that wasn't encrypted. Please uncheck the password box.",
......
......@@ -263,7 +263,7 @@
"export_instance": "Exporter l'instance",
"go_export_instance": "Exporter",
"exporting": "Export en cours…",
"export_instance_help": "Cela va exporter l'instance entière dans un format qu'une autre instance de Kresus peut par la suite réimporter. Vous pouvez choisir d'exporter vers un fichier JSON en clair (qui ne contiendra pas vos mots de passes bancaires) ou en utilisant un mot de passe vers un fichier texte chiffré (qui contiendra vos mots de passe bancaires). Si vous choisissez un export chiffré, le mot de passe choisi devra contenir au moins 8 caractères, dont une lettre en majuscule, une lettre en minuscule et un chiffre.",
"export_instance_help": "Cela va exporter l'instance entière dans un format qu'une autre instance de Kresus peut par la suite réimporter. Vous pouvez exporter vers un fichier JSON en clair (qui ne contiendra pas vos mots de passes bancaires). Si la personne administrant votre Kresus l'a activé, vous pourrez également effectuer un export chiffré avec un mot de passe (qui contiendra vos mots de passe bancaires) ; dans ce cas, le mot de passe choisi devra contenir au moins 8 caractères, dont une lettre en majuscule, une lettre en minuscule et un chiffre.",
"encrypt_with_password": "Chiffrer avec le mot de passe",
"weak_password": "Mot de passe trop faible !",
"error_decrypt_non_encrypted": "Vous essayez de déchiffrer un import non chiffré. Veuillez déselectionner la case mot de passe s'il vous plaît.",
......
......@@ -36,11 +36,7 @@ let MIGRATIONS = null;
before(async function() {
// Set process.kresus.user for models.
applyConfig({
kresus: {
salt: 'pepper is even better than salt amirite'
}
});
applyConfig({});
// Set a temporary database for testing.
let options = {
......
......@@ -3,7 +3,6 @@ import ini from 'ini';
import ospath from 'ospath';
import path from 'path';
import should from 'should';
import updeep from 'updeep';
import { apply as applyConfig } from '../../server/config';
......@@ -40,6 +39,7 @@ function checkCommonDefaultConfig(env) {
env.port.should.equal(9876);
env.host.should.equal('127.0.0.1');
env.pythonExec.should.equal('python2');
should.not.exist(env.salt);
should.not.exist(env.weboobDir);
should.not.exist(env.weboobSourcesList);
should.not.exist(env.emailTransport);
......@@ -53,17 +53,6 @@ function checkCommonDefaultConfig(env) {
env.smtpRejectUnauthorizedTLS.should.equal(true);
}
const CONFIG_SALT = {
kresus: {
salt: '1234567890123456'
}
};
function applyConfigWithSalt(config) {
let actualConfig = updeep(CONFIG_SALT, config);
applyConfig(actualConfig);
}
describe('Test the configuration file is correctly taken into account', () => {
// If the path to Weboob is set, if will override the configuration, we then skip these tests
// if KRESUS_WEBOOB_DIR is set.
......@@ -74,28 +63,19 @@ describe('Test the configuration file is correctly taken into account', () => {
});
describe('Test default configuration', () => {
it('not passing a salt value is an error', () => {
it('a partially incomplete configuration should get the default keys', () => {
process.kresus = {};
should.throws(() => applyConfig({}));
should.throws(() => applyConfig({ kresus: {} }));
should.throws(() => applyConfig({ kresus: { salt: '123456789012345' } }));
applyConfig(CONFIG_SALT);
// No configuration object means an empty configuration object.
applyConfig();
checkHasConfigKeys(process.kresus);
checkCommonDefaultConfig(process.kresus);
process.kresus.dataDir.should.equal(path.join(ospath.home(), '.kresus'));
process.kresus.urlPrefix.should.equal('/');
});
it('a partially incomplete configuration should get the default keys', () => {
process.kresus = {};
// Empty configuration object.
let config = {};
applyConfigWithSalt(config);
applyConfig(config);
checkHasConfigKeys(process.kresus);
checkCommonDefaultConfig(process.kresus);
......@@ -106,7 +86,7 @@ describe('Test the configuration file is correctly taken into account', () => {
email: {}
};
applyConfigWithSalt(config);
applyConfig(config);
checkHasConfigKeys(process.kresus);
checkCommonDefaultConfig(process.kresus);
......@@ -119,7 +99,7 @@ describe('Test the configuration file is correctly taken into account', () => {
}
};
applyConfigWithSalt(config);
applyConfig(config);
checkHasConfigKeys(process.kresus);
process.kresus.port.should.equal(4242);
......@@ -151,7 +131,7 @@ describe('Test the configuration file is correctly taken into account', () => {
it('shall return correct default config', () => {
process.kresus = {};
applyConfigWithSalt(config);
applyConfig(config);
checkHasConfigKeys(process.kresus);
checkCommonDefaultConfig(process.kresus);
......@@ -354,65 +334,83 @@ describe('Test the configuration file is correctly taken into account', () => {
it('shall throw when Kresus port is out of range', () => {
(function negativePort() {
process.kresus = {};
applyConfigWithSalt({ kresus: { port: -1 } });
applyConfig({ kresus: { port: -1 } });
}.should.throw());
(function negativePortEnv() {
process.kresus = {};
process.env.PORT = '-1';
applyConfigWithSalt();
applyConfig();
}.should.throw());
delete process.env.PORT;
(function zeroPort() {
process.kresus = {};
applyConfigWithSalt({ kresus: { port: 0 } });
applyConfig({ kresus: { port: 0 } });
}.should.throw());
(function zeroPortEnv() {
process.kresus = {};
process.env.PORT = '0';
applyConfigWithSalt();
applyConfig();
}.should.throw());
delete process.env.PORT;
(function overflowPort() {
process.kresus = {};
applyConfigWithSalt({ kresus: { port: 65536 } });
applyConfig({ kresus: { port: 65536 } });
}.should.throw());
(function stringPort() {
process.kresus = {};
applyConfigWithSalt({ kresus: { port: 'ALO UI CER LE BUG' } });
applyConfig({ kresus: { port: 'ALO UI CER LE BUG' } });
}.should.throw());
});
it('shall throw when SMTP port is out of range', () => {
(function negativePort() {
process.kresus = {};
applyConfigWithSalt({ email: { port: -1 } });
applyConfig({ email: { port: -1 } });
}.should.throw());
(function zeroPort() {
process.kresus = {};
applyConfigWithSalt({ email: { port: 0 } });
applyConfig({ email: { port: 0 } });
}.should.throw());
(function overflowPort() {
process.kresus = {};
applyConfigWithSalt({ email: { port: 65536 } });
applyConfig({ email: { port: 65536 } });
}.should.throw());
(function stringPort() {
process.kresus = {};
applyConfigWithSalt({ email: { port: 'COUCOU TU VEUX VOIR MON BUG' } });
applyConfig({ email: { port: 'COUCOU TU VEUX VOIR MON BUG' } });
}.should.throw());
});
it('shall throw when email transport is not smtp or sendmail', () => {
(function negativePort() {
process.kresus = {};
applyConfigWithSalt({ email: { transport: 'foobar' } });
applyConfig({ email: { transport: 'foobar' } });
}.should.throw());
});
it("shall throw when a non-empty salt doesn't fit the criteria", () => {
// An empty string is not taken into account.
process.kresus = {};
applyConfig({ kresus: { salt: '' } });
checkHasConfigKeys(process.kresus);
checkCommonDefaultConfig(process.kresus);
(function tooShort1() {
process.kresus = {};
applyConfig({ kresus: { salt: 'a' } });
}.should.throw());
(function tooShort15() {
process.kresus = {};
applyConfig({ kresus: { salt: '123456789012345' } });
}.should.throw());
});
});
......
......@@ -200,7 +200,7 @@ describe('Testing kresus/weboob integration', function() {
describe('with weboob not installed.', () => {
it('call "test" should raise "WEBOOB_NOT_INSTALLED" error, if weboob is not globally installed. WARNING: if this test fails, make sure Weboob is not installed globally before opening an issue.', async () => {
applyConfig({ kresus: { salt: '1234567890123456' } });
applyConfig({});
// Simulate the non installation of weboob.
process.kresus.weboobDir = null;
let result = await callWeboobBefore('test');
......@@ -218,7 +218,7 @@ describe('Testing kresus/weboob integration', function() {
describe('Defect situations', () => {
describe('call an unknown command', () => {
it('should raise "INTERNAL_ERROR" error', async () => {
applyConfig({ kresus: { salt: '1234567890123456' } });
applyConfig({});
let result = await callWeboobBefore('unknown-command');
checkError(result, INTERNAL_ERROR);
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment