Skip to content

Commit

Permalink
feat: Define storage and encryption handlers.
Browse files Browse the repository at this point in the history
  • Loading branch information
dipasqualew committed Aug 3, 2020
1 parent a5cdf1c commit e6d6a0f
Show file tree
Hide file tree
Showing 19 changed files with 6,761 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "dipasqualew",
"parserOptions": {
"sourceType": "module"
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
12 changes: 12 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};
6,269 changes: 6,269 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
"author": "William Di Pasquale <dipasquale.w@gmail.com>",
"license": "MIT",
"scripts": {
"test": "echo test"
"test": "jest"
},
"devDependencies": {
"@babel/core": "^7.11.0",
"@babel/preset-env": "^7.11.0",
"babel-jest": "^26.2.2",
"eslint": "^7.6.0",
"eslint-config-dipasqualew": "^1.0.0",
"jest": "^26.2.2",
"sinon": "^9.0.2",
"text-encoding-utf-8": "^1.0.2"
},
"dependencies": {
"openpgp": "^4.10.7"
}
}
File renamed without changes.
120 changes: 120 additions & 0 deletions src/storage/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@

/**
* Abstract StorageHandler
* with common utility methods
* shared between all storage handlers.
*/
export default class StorageHandler {

/**
* Creates a StorageHandler
* @param {string} key
* @param {object} di
* @param {StrategyHandler} di.strategy
*/
constructor(key, di) {
this.key = key;
this.di = di;
}

/**
* Serializes a payload so that
* it can be saved to the storage.
*
* @param {object} context
* @param {object} payload
*
* @returns {object}
*/
async serialize(context, payload) {
const output = {
meta: {
key: this.key,
serializer: this.constructor.name,
version: this.constructor.version,
datetime: Date.now(),
},
payload: await this.di.strategy.encrypt(context, JSON.stringify(payload)),
};

return output;
}

/**
* Deserializes a serialized and
* encrypted object, so that it can be
* loaded into memory.
*
* @param {object} context
* @param {object} serialized
*/
async deserialize(context, serialized) {
if (!serialized.meta) {
throw new Error('Invalid serialized data.');
}

if (serialized.meta.serializer !== this.constructor.name) {
throw new Error(`Invalid serializer: ${serialized.meta.serialized}`);
}

if (serialized.meta.version !== this.constructor.version) {
throw new Error(`Version mismatch. Data is ${serialized.meta.version}, serializer is ${this.constructor.version}`);
}

const payload = JSON.parse(await this.di.strategy.decrypt(context, serialized.payload));

return payload;
}

/**
* Serializes and saves to the storage
* the provided payload.
*
* @param {object} context
* @param {object} payload
* @returns {object}
*/
async save(context, payload) {
const serialized = await this.serialize(context, payload);
await this.implSave(context, serialized);

return serialized;
}

/**
* Loads from storage the payload
* and deserializes it.
*
* @param {object} context
* @returns {object}
*/
async load(context) {
const serialized = await this.implLoad();
return await this.deserialize(context, serialized);
}

/**
* Saves the data into the storage.
* Override this method.
*
* @param {object} _context
* @param {object} _data
*/
async implSave(_context, _data) {
throw new Error('Not implemented.');
}

/**
* Loads the data from the storage.
* Override this method.
*
* @param {object} _context
* @returns {object}
*/
async implLoad(_context) {
throw new Error('Not implemented.');
}

}

StorageHandler.version = 1;
30 changes: 30 additions & 0 deletions src/storage/localStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

import StorageHandler from './handler';


export default class LocalStorageHandler extends StorageHandler {

/**
* Returns the LocalStorage instance.
*
* @returns {WindowLocalStorage}
*/
get ls() {
return this.context.localStorage;
}

/**
* @inheritdoc
*/
async implSave(context, data) {
this.ls.setItem(this.key, data);
}

/**
* @inheritdoc
*/
implLoad() {
return this.ls.getItem(this.key);
}

}
12 changes: 12 additions & 0 deletions src/strategy/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@


export default class StrategyHandler {

encrypt(_plaintext, _config = {}) {
throw new Error('Not implemented error.');
}

decrypt(_encrypted, _config = {}) {
throw new Error('Not implemented error.');
}
}
47 changes: 47 additions & 0 deletions src/strategy/pgp/password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@

import { message, decrypt, encrypt } from 'openpgp';

import StrategyHandler from '../handler';

/**
* PGP Password Handler
*/
export default class PGPPasswordHandler extends StrategyHandler {
/**
* Encrypts a string with the given password.
*
* @param {object} context
* @param {string} context.password
* @param {string} plaintext
* @returns {string}
*/
async encrypt(context, plaintext) {
const options = {
message: message.fromText(plaintext),
passwords: [context.password],
armor: true,
};

const output = await encrypt(options);
return output.data;
}

/**
* Decrypts a string with the given password.
*
* @param {object} context
* @param {string} context.password
* @param {string} encrypted
* @returns {string}
*/
async decrypt(context, encrypted) {
const options = {
message: await message.readArmored(encrypted),
passwords: [context.password],
format: 'utf8',
};

const output = await decrypt(options);
return output.data;
}
}
6 changes: 6 additions & 0 deletions tests/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": [
"../.eslintrc",
"dipasqualew/mixins/testing/mocha"
]
}
43 changes: 43 additions & 0 deletions tests/mocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import sinon from 'sinon';


/**
* Generates a dependency injector
*
* @param {function?} encrypt
* @param {function?} decrypt
* @returns {object}
*/
export const getDi = (encrypt = null, decrypt = null) => {

if (!encrypt) {
encrypt = sinon.fake.resolves('encrypted');
}

if (!decrypt) {
decrypt = sinon.fake.resolves('decrypted');
}

return {
strategy: {
encrypt,
decrypt,
},
};
};

export const getStorageHandler = (kls, key = 'StorageHandlerKey', di = null) => {
if (!di) {
di = getDi();
}

return new kls(key, di);
};

export const getLocalStorage = () => {
const ls = { __data__: {} };
ls.getItem = (key) => ls.__data__[key];
ls.setItem = (key, value) => (ls.__data__[key] = value);

return ls;
};
62 changes: 62 additions & 0 deletions tests/unit/integration/match.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import sinon from 'sinon';

import LocalStorageHandler from '../../../src/storage/localStorage';
import PGPPasswordHandler from '../../../src/strategy/pgp/password';
import { getLocalStorage } from '../../mocks';

const integrationMatchTest = (getStrategy, getStorage) => {
beforeAll(() => {
const textEncoding = require('text-encoding-utf-8');
global.TextEncoder = textEncoding.TextEncoder;

});

const strategyName = getStrategy().constructor.name;
const storageName = getStorage().constructor.name;

describe(`${storageName} + ${strategyName}`, () => {

it('Encrypts and decrypts the payload', async () => {
const strategy = getStrategy();
const storage = getStorage(strategy);

const password = 'My Password';
const payload = {
key1: 'value1',
nested: {
key2: 'value2',
},
};

await storage.save({ password }, payload);
const actual = await storage.load({ password });

return expect(payload).toEqual(actual);
});
});
};


const getPGPPasswordHandler = () => new PGPPasswordHandler();

const getLocalStorageHandler = (strategy) => {
const ls = getLocalStorage();
const lsh = new LocalStorageHandler('lsh', { strategy });
sinon.stub(lsh, 'ls').get(() => ls);

return lsh;
};

const strategies = [
getPGPPasswordHandler,
];

const storages = [
getLocalStorageHandler,
];

strategies.forEach((getStrategy) => {
storages.forEach((getStorage) => {
integrationMatchTest(getStrategy, getStorage);
});
});
13 changes: 13 additions & 0 deletions tests/unit/storage/__snapshots__/handler.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`serialize Serializes the payload 1`] = `
Object {
"meta": Object {
"datetime": Any<Number>,
"key": "StorageHandlerKey",
"serializer": "StorageHandler",
"version": 1,
},
"payload": "encrypted",
}
`;
13 changes: 13 additions & 0 deletions tests/unit/storage/__snapshots__/localStorage.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`serialize Serializes the payload 1`] = `
Object {
"meta": Object {
"datetime": Any<Number>,
"key": "StorageHandlerKey",
"serializer": "LocalStorageHandler",
"version": 1,
},
"payload": "encrypted",
}
`;
Loading

0 comments on commit e6d6a0f

Please sign in to comment.