Added encryption infrastructure

Signed-off-by: Marius David Wieschollek's avatarMarius David Wieschollek <passwords.public@mdns.eu>
parent d1db486a
import sodium from 'libsodium-wrappers';
export default class PWDv1Challenge {
constructor(data) {
this._salts = null;
if(data.hasOwnProperty('salts')) {
this._salts = data.salts;
}
this._password = null;
}
/**
*
* @returns {null}
*/
getPassword() {
return this._password;
}
/**
*
* @param value
* @returns {PWDv1Challenge}
*/
setPassword(value) {
this._password = value;
return this;
}
/**
* Generate a challenge solution with the user provided password
* and the server provided salts
*
* @returns {string}
*/
solve() {
if(this._password.length < 12) throw new Error('Password is too short');
if(this._password.length > 128) throw new Error('Password is too long');
let salts = this._salts;
let passwordSalt = sodium.from_hex(salts[0]),
genericHashKey = sodium.from_hex(salts[1]),
passwordHashSalt = sodium.from_hex(salts[2]),
genericHash = sodium.crypto_generichash(
sodium.crypto_generichash_BYTES_MAX,
new Uint8Array([...sodium.from_string(this._password), ...passwordSalt]),
genericHashKey
);
let passwordHash = sodium.crypto_pwhash(
sodium.crypto_box_SEEDBYTES,
genericHash,
passwordHashSalt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_DEFAULT
);
return sodium.to_hex(passwordHash);
}
/**
* Create the salts and the secret for the server
* using the user provided password
*
* @returns {{salts: *[], secret: *}}
*/
create() {
if(this._password.length < 12) throw new Error('Password is too short');
if(this._password.length > 128) throw new Error('Password is too long');
let passwordSalt = this._generateRandom(256),
genericHashKey = this._generateRandom(sodium.crypto_generichash_KEYBYTES_MAX),
genericHash = sodium.crypto_generichash(
sodium.crypto_generichash_BYTES_MAX,
new Uint8Array([...sodium.from_string(this._password), ...passwordSalt]),
genericHashKey
);
let passwordHashSalt = this._generateRandom(sodium.crypto_pwhash_SALTBYTES),
passwordHash = sodium.crypto_pwhash(
sodium.crypto_box_SEEDBYTES,
genericHash,
passwordHashSalt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_DEFAULT
);
return {
salts : [
sodium.to_hex(passwordSalt),
sodium.to_hex(genericHashKey),
sodium.to_hex(passwordHashSalt)
],
secret: sodium.to_hex(passwordHash)
}
}
// noinspection JSMethodCanBeStatic
/**
*
* @param length
* @returns {Uint8Array}
* @private
* @deprecated
*/
_generateRandom(length) {
let array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return array;
}
}
\ No newline at end of file
import PWDv1Challenge from './Challenge/PWDv1Challenge';
import CSEv1Keychain from '../Encryption/Keychain/CSEv1Keychain';
export default class SessionAuthorisation {
/**
*
* @param {Server} server
*/
constructor(server) {
this._server = server;
this._challenge = null;
this._token = null;
}
async load() {
let response = await this._server.createRequest()
.setPath('api/1.0/session/request')
.send();
let requirements = response.getData();
if(requirements.hasOwnProperty('challenge')) {
this._challenge = new PWDv1Challenge(requirements.challenge)
}
return this;
}
hasChallenge() {
return this._challenge !== null;
}
/**
*
* @returns {PWDv1Challenge}
*/
getChallenge() {
return this._challenge;
}
hasToken() {
return this._token !== null;
}
getToken() {
return this._token;
}
async authorize(password, token) {
let data = {};
if(this.hasChallenge()) {
if(password) {
data.challenge = this._challenge
.setPassword(password)
.solve();
} else {
data.challenge = this._challenge.solve();
}
}
let request = await this._server.createRequest()
.setPath('api/1.0/session/open')
.setData(data);
let response = await request.send();
if(response.getData().success) {
if(this.hasChallenge()) {
let keychain = new CSEv1Keychain(response.getData().keys.CSEv1r1, this._challenge.getPassword());
this._server.getCseV1Encryption().setKeychain(keychain);
}
request.getSession().setAuthorized(true);
}
}
}
\ No newline at end of file
import sodium from 'libsodium-wrappers';
import BooleanState from '../State/BooleanState';
export default class CSEv1Encryption {
constructor() {
this.fields = {
password: ['url', 'label', 'notes', 'password', 'username', 'customFields'],
folder : ['label'],
tag : ['label', 'color']
};
this._enabled = new BooleanState(false);
this._keychain = null;
}
/**
*
* @returns {Promise<boolean>}
*/
async ready() {
await sodium.ready && await this._enabled.awaitTrue();
}
/**
* Encrypts an object
*
* @param {Object} object
* @param {string} type
* @returns {Object}
*/
async encrypt(object, type) {
if(!this.fields.hasOwnProperty(type)) throw new Error('Invalid object type');
await this.ready();
let fields = this.fields[type],
key = this._keychain.getCurrentKey();
for(let i = 0; i < fields.length; i++) {
let field = fields[i],
data = object[field];
if(data === null || data.length === 0) continue;
object[field] = this._encryptString(data, key);
}
object.cseType = 'CSEv1r1';
object.cseKey = this._keychain.getCurrentKeyId();
return object;
}
/**
* Decrypts an object
*
* @param {Object} object
* @param {string} type
* @returns {Object}
*/
async decrypt(object, type) {
if(!this.fields.hasOwnProperty(type)) throw new Error('Invalid object type');
if(object.cseType !== 'CSEv1r1') throw new Error('Unsupported encryption type');
await this.ready();
let fields = this.fields[type],
key = this._keychain.getKey(object.cseKey);
for(let i = 0; i < fields.length; i++) {
let field = fields[i],
data = object[field];
if(data === null || data.length === 0) continue;
object[field] = this._decryptString(data, key);
}
return object;
}
/**
* Encrypt the message with the given key and return a base64 encoded string
*
* @param {string} message
* @param {Uint8Array} key
* @returns {string}
* @private
*/
_encryptString(message, key) {
return sodium.to_base64(this._encrypt(message, key));
}
/**
* Decrypt the base64 encoded message with the given key
*
* @param {string} encodedString
* @param {Uint8Array} key
* @returns {string}
* @private
*/
_decryptString(encodedString, key) {
let encryptedString = sodium.from_base64(encodedString);
return sodium.to_string(this._decrypt(encryptedString, key));
}
/**
* Encrypt the message with the given key
*
* @param {Uint8Array} message
* @param {Uint8Array} key
* @returns {Uint8Array}
* @private
*/
_encrypt(message, key) {
let nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
return new Uint8Array([...nonce, ...sodium.crypto_secretbox_easy(message, nonce, key)]);
}
/**
* Decrypt the message with the given key
*
* @param {Uint8Array} encrypted
* @param {Uint8Array} key
* @returns {Uint8Array}
* @private
*/
_decrypt(encrypted, key) {
if(encrypted.length < sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) throw new Error('Invalid encrypted text length');
let nonce = encrypted.slice(0, sodium.crypto_secretbox_NONCEBYTES),
ciphertext = encrypted.slice(sodium.crypto_secretbox_NONCEBYTES);
return sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
}
/**
* Decrypt and activate the keychain
*
* @param {CSEv1Keychain} keychain
* @private
*/
setKeychain(keychain) {
this._keychain = keychain;
keychain
.ready()
.then(() => {
this._enabled.set(true);
});
}
/**
* Remove the current keychain
*/
unsetKeychain() {
this._enabled.set(false);
this._keychain = null;
}
}
\ No newline at end of file
import sodium from 'libsodium-wrappers';
import CSEv1Encryption from './CSEv1Encryption';
export default class ExportV1Encryption extends CSEv1Encryption{
/**
* Encrypts the message with the user defined password
*
* @param message
* @param password
* @returns {*}
*/
encryptWithPassword(message, password) {
let salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES),
key = this._passwordToKey(password, salt),
encrypted = this._encrypt(message, key);
return sodium.to_base64(new Uint8Array([...salt, ...encrypted]));
}
/**
* Decrypts the message with the user defined password
*
* @param message
* @param password
* @returns {*}
*/
decryptWithPassword(message, password) {
let encrypted = sodium.from_base64(message),
salt = encrypted.slice(0, sodium.crypto_pwhash_SALTBYTES),
text = encrypted.slice(sodium.crypto_pwhash_SALTBYTES),
key = this._passwordToKey(password, salt);
return sodium.to_string(this._decrypt(text, key));
}
}
\ No newline at end of file
import sodium from 'libsodium-wrappers';
import uuid from 'uuidv4';
import BooleanState from '../../State/BooleanState';
export default class CSEv1Keychain {
constructor(keychain = null, password = null) {
this._keys = {};
this._current = null;
this._enabled = new BooleanState(false);
this._password = password;
if(keychain !== null) {
sodium.ready.then(() => {
this.import(keychain);
});
}
}
/**
*
* @returns {Promise<boolean>}
*/
async ready() {
return await this._enabled.awaitTrue();
}
/**
* Set the password to encrypt/decrypt the keychain
* @param value
* @returns {CSEv1Keychain}
*/
setPassword(value) {
this._password = value;
return this;
}
/**
* Get a key by id
*
* @param id
* @returns {string}
*/
getKey(id) {
if(this._keys.hasOwnProperty(id)) {
return this._keys[id];
}
throw new Error('Unknown CSE key id');
}
/**
* Get the current key
*
* @returns {string}
*/
getCurrentKey() {
return this.getKey(this._current);
}
/**
* Get the current key
*
* @returns {string|null}
*/
getCurrentKeyId() {
return this._current;
}
/**
* Decrypt the given keychain and apply it
*
* @param {string} keychainText
*/
import(keychainText) {
let encrypted = sodium.from_base64(keychainText),
salt = encrypted.slice(0, sodium.crypto_pwhash_SALTBYTES),
text = encrypted.slice(sodium.crypto_pwhash_SALTBYTES),
key = this._passwordToKey(this._password, salt),
keychain = JSON.parse(sodium.to_string(this._decrypt(text, key)));
for(let id in keychain.keys) {
if(keychain.keys.hasOwnProperty(id)) {
this._keys[id] = sodium.from_hex(keychain.keys[id]);
}
}
this._current = keychain.current;
this._enabled.set(true);
return this;
}
/**
* Export the keychain as encrypted string
*
* @returns {string}
*/
export() {
let keychain = {
keys : {},
current: this._current
};
for(let id in this._keys) {
if(this._keys.hasOwnProperty(id)) {
keychain.keys[id] = sodium.to_hex(this._keys[id]);
}
}
let salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES),
key = this._passwordToKey(this._password, salt),
encrypted = this._encrypt(JSON.stringify(keychain), key);
return sodium.to_base64(new Uint8Array([...salt, ...encrypted]));
}
/**
* Add a new key to the keychain and set it as current
*/
update() {
let uuid = uuid();
this._keys[uuid] = sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES);
this._current = uuid;
this._enabled.set(true);
}
/**
* Encrypt the message with the given key
*
* @param message
* @param key
* @returns {Uint8Array}
*/
_encrypt(message, key) {
let nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
return new Uint8Array([...nonce, ...sodium.crypto_secretbox_easy(message, nonce, key)]);
}
// noinspection JSMethodCanBeStatic
/**
* Decrypt the message with the given key
*
* @param encrypted
* @param key
* @returns {Uint8Array}
*/
_decrypt(encrypted, key) {
if(encrypted.length < sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) throw new Error('Invalid encrypted text length');
let nonce = encrypted.slice(0, sodium.crypto_secretbox_NONCEBYTES),
ciphertext = encrypted.slice(sodium.crypto_secretbox_NONCEBYTES);
return sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
}
// noinspection JSMethodCanBeStatic
/**
*
* @param password
* @param salt
* @returns {Uint8Array}
* @private
*/
_passwordToKey(password, salt) {
return sodium.crypto_pwhash(
sodium.crypto_box_SEEDBYTES,
password,
salt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_DEFAULT
);
}
}
\ No newline at end of file
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