This repository has been archived on 2024-10-24. You can view files and clone it, but cannot push or open issues or pull requests.
fradrive-old/frontend/src/lib/storage-manager/storage-manager.js

390 lines
12 KiB
JavaScript

/* global global:writable */
import * as semver from 'semver';
import sodium from 'sodium-javascript';
import { HttpClient } from '../../services/http-client/http-client';
export const LOCATION = {
LOCAL: 'local',
SESSION: 'session',
WINDOW: 'window',
};
const LOCATION_SHADOWING = [ LOCATION.WINDOW, LOCATION.SESSION, LOCATION.LOCAL ];
export class StorageManager {
namespace;
version;
_options;
_global;
_encryptionKey = {};
constructor(namespace, version, options) {
this.namespace = namespace;
this.version = semver.valid(version);
if (!namespace) {
throw new Error('Cannot setup StorageManager without namespace');
}
if (!this.version) {
throw new Error('Cannot setup StorageManager without valid semver version');
}
if (options !== undefined) {
this._options = options;
}
if (global !== undefined)
this._global = global;
else if (window !== undefined)
this._global = window;
else
throw new Error('Cannot setup StorageManager without window or global');
if (this._options.encryption) {
[LOCATION.LOCAL, LOCATION.SESSION].forEach((location) => {
const encryption = this._options.encryption.all || this._options.encryption[location];
if (encryption) this._requestStorageKey({ location: location, encryption: encryption });
});
}
}
save(key, value, options=this._options) {
if (!key) {
throw new Error('StorageManager.save called with invalid key');
}
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
throw new Error('StorageManager.save called with unsupported location option');
}
const location = options && options.location !== undefined ? options.location : LOCATION_SHADOWING[0];
switch (location) {
case LOCATION.LOCAL: {
this._saveToLocalStorage(this._updateStorage(this._getFromLocalStorage(options), { [key]: value }, LOCATION.LOCAL, options));
break;
}
case LOCATION.SESSION: {
this._saveToSessionStorage(this._updateStorage(this._getFromSessionStorage(options), { [key]: value }, LOCATION.SESSION, options));
break;
}
case LOCATION.WINDOW: {
this._saveToWindow({ ...this._getFromWindow(), [key]: value });
break;
}
default:
console.error('StorageManager.save cannot save item with unsupported location');
}
}
load(key, options=this._options) {
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
throw new Error('StorageManager.load called with unsupported location option');
}
let locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;
while (locations.length > 0) {
const location = locations.shift();
let val;
switch (location) {
case LOCATION.LOCAL: {
val = this._getFromLocalStorage(options)[key];
break;
}
case LOCATION.SESSION: {
val = this._getFromSessionStorage(options)[key];
break;
}
case LOCATION.WINDOW: {
val = this._getFromWindow()[key];
break;
}
default:
console.error('StorageManager.load cannot load item with unsupported location');
}
if (val !== undefined || locations.length === 0) {
return val;
}
}
}
remove(key, options=this._options) {
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
throw new Error('StorageManager.load called with unsupported location option');
}
const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;
for (const location of locations) {
switch (location) {
case LOCATION.LOCAL: {
let val = this._getFromLocalStorage(options);
delete val[key];
return this._saveToLocalStorage(val);
}
case LOCATION.SESSION: {
let val = this._getFromSessionStorage(options);
delete val[key];
return this._saveToSessionStorage(val);
}
case LOCATION.WINDOW: {
let val = this._getFromWindow();
delete val[key];
return this._saveToWindow(val);
}
default:
console.error('StorageManager.load cannot load item with unsupported location');
}
}
}
clear(options) {
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
throw new Error('StorageManager.clear called with unsupported location option');
}
const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;
for (const location of locations) {
switch (location) {
case LOCATION.LOCAL:
return this._clearLocalStorage();
case LOCATION.SESSION:
return this._clearSessionStorage();
case LOCATION.WINDOW:
return this._clearWindow();
default:
console.error('StorageManager.clear cannot clear with unsupported location');
}
}
}
_getFromLocalStorage(options=this._options) {
let state;
try {
state = JSON.parse(window.localStorage.getItem(this.namespace));
} catch {
state = null;
}
if (state === null || !state.version || !semver.satisfies(this.version, `^${state.version}`)) {
// remove item from localStorage if it stores an invalid state
this._clearLocalStorage();
return {};
}
if ('state' in state)
return this._getFromStorage(state.state, LOCATION.LOCAL, options);
else {
delete state.version;
return this._getFromStorage(state, LOCATION.LOCAL, options);
}
}
_saveToLocalStorage(state) {
if (!state)
return this._clearLocalStorage();
let versionedState;
if ('version' in state || 'state' in state) {
versionedState = { version: this.version, state: state };
} else {
versionedState = { version: this.version, ...state };
}
window.localStorage.setItem(this.namespace, JSON.stringify(versionedState));
}
_clearLocalStorage() {
window.localStorage.removeItem(this.namespace);
}
_getFromWindow() {
if (!this._global || !this._global.App)
return {};
if (!this._global.App.Storage)
this._global.App.Storage = {};
return this._global.App.Storage;
}
_saveToWindow(value) {
if (!this._global || !this._global.App) {
return console.error('StorageManager._saveToWindow called when window.App is not available');
}
if (!value)
return this._clearWindow();
if (!this._global.App.Storage)
this._global.App.Storage = {};
this._global.App.Storage[this.namespace] = value;
}
_clearWindow() {
if (!this._global || !this._global.App) {
return console.error('StorageManager._saveToWindow called when window.App is not available');
}
if (this._global.App.Storage) {
delete this._global.App.Storage[this.namespace];
}
}
_getFromSessionStorage(options=this._options) {
console.log('_getFromSessionStorage with args', options);
let state;
try {
state = JSON.parse(window.sessionStorage.getItem(this.namespace));
} catch {
state = null;
}
if (state === null || !state.version || !semver.satisfies(this.version, `^${state.version}`)) {
// remove item from session if it stores an invalid state
this._clearSessionStorage();
return {};
}
if ('state' in state)
return this._getFromStorage(state.state, LOCATION.SESSION, options);
else {
delete state.version;
return this._getFromStorage(state, LOCATION.SESSION, options);
}
}
_saveToSessionStorage(state) {
if (!state)
return this._clearSessionStorage();
let versionedState;
if ('version' in state || 'state' in state) {
versionedState = { version: this.version, state: state };
} else {
versionedState = { version: this.version, ...state };
}
window.sessionStorage.setItem(this.namespace, JSON.stringify(versionedState));
}
_clearSessionStorage() {
window.sessionStorage.removeItem(this.namespace);
}
_getFromStorage(storage, location, options=this._options) {
const encryption = options.encryption && (options.encryption.all || options.encryption[location]);
if (encryption && storage.encryption) {
return { ...storage, ...JSON.parse(decrypt(storage.encryption.ciphertext, this._encryptionKey[location]) || '{}') };
} else {
return storage;
}
}
_updateStorage(storage, update, location, options=this._options) {
const encryption = options.encryption && (options.encryption.all || options.encryption[location]);
if (encryption && storage.encryption) {
const updatedDecryptedStorage = { ...JSON.parse(decrypt(storage.encryption.ciphertext, this._encryptionKey[location]) || '{}'), ...update };
return { ...storage, encryption: { ...storage.encryption, ciphertext: encrypt(JSON.stringify(updatedDecryptedStorage), this._encryptionKey[location]) } };
} else {
return { ...storage, ...update };
}
}
_requestStorageKey(options=this._options) {
if (!(options && options.location && options.encryption))
throw new Error('Storage Manager cannot request storage key with unsupported options!');
const requestBody = {
type : options.encryption,
length : 42,
...this.load('encryption', { ...options, encryption: false }),
};
this._global.App.httpClient.post({
url: '../../../../../../user/storage-key', // TODO use APPROOT instead
headers: {
'Content-Type' : HttpClient.ACCEPT.JSON,
'Accept' : HttpClient.ACCEPT.JSON,
},
body: JSON.stringify(requestBody),
}).then(
(response) => response.json()
).then((response) => {
console.log('storage-manager got key from response:', response, 'with options:', options);
if (response.salt !== requestBody.salt || response.timestamp !== requestBody.timestamp) {
this.clear(options);
}
this.save('encryption', { salt: response.salt, timestamp: response.timestamp }, { ...options, encryption: false });
this._encryptionKey[options.location] = response.key;
}).catch(console.error);
}
}
// TODO debug unnecessary calls of encrypt
function encrypt(plaintext, key) {
console.log('args encrypt', plaintext, key);
if (!plaintext) return '';
if (!key) throw new Error('Cannot encrypt plaintext without a valid key!');
/* eslint-disable no-undef */
// TODO use const if possible
let plaintextB = Buffer.from(plaintext);
let cipherB = Buffer.alloc(plaintextB.length + sodium.crypto_secretbox_MACBYTES);
let nonceB = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES);
let keyB = Buffer.from(key);
/* eslint-enable no-undef */
sodium.crypto_secretbox_easy(cipherB, plaintextB, nonceB, keyB);
return cipherB;
}
// TODO debug unnecessary calls of decrypt
function decrypt(ciphertext, key) {
console.log('args decrypt', ciphertext, key);
if (!ciphertext) return '';
if (!key) throw new Error('Cannot decrypt ciphertext without a valid key!');
/* eslint-disable no-undef */
// TODO use const if possible
let cipherB = Buffer.from(ciphertext);
let plaintextB = Buffer.alloc(cipherB.length - sodium.crypto_secretbox_MACBYTES);
let nonceB = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES);
let keyB = Buffer.from(key);
/* eslint-enable no-undef */
sodium.crypto_secretbox_open_easy(plaintextB, cipherB, nonceB, keyB);
return plaintextB.toString();
}