StorageAdapters/LocalStorageAdapter.js

import StorageAdapter from './StorageAdapter'
import {InvalidPassword, AccountNotFoundError} from '../Errors'
import CryptoJS from 'crypto-js';
import crypto from 'crypto'
const hash = crypto.createHash('sha256');

const AES_CONFIG = {
	mode: CryptoJS.mode.CTR,
	padding: CryptoJS.pad.Iso10126,
	iterations: 5
};

if (typeof window === "undefined" || typeof window.localStorage === "undefined") {
	if (typeof localStorage === "undefined") {
		var LocalStorage = require('node-localstorage').LocalStorage;
		var localStorage = new LocalStorage('./localStorage');
	}
} else {
	localStorage = window.localStorage
}

/**
 * LocalStorageAdapter allows saving of Wallets to the users local computer if they don't wish to store it on a Keystore server.
 * @extends {StorageAdapter}
 */
class LocalStorageAdapter extends StorageAdapter {
	/**
	 * Create a new LocalStorageAdapter
	 * @param  {string} username - The username of the account you wish to use
	 * @param  {string} password - The password of the account you wish to use
	 * @return {LocalStorageAdapter}
	 */
	constructor(username, password) {
		super(username, password)
	}

	/**
	 * Load the Account from LocalStorage
	 *
	 * @async
	 * @throws {InvalidPassword} If the password being used for login is invalid
	 * @throws {AccountNotFoundError} If the Account cannot beb found on the storage server
	 * @return {Promise<Object>} Returns a Promise that will resolve to the Decrypted Account Data if successful
	 */
	async load() {
		let id

		try {
			id = await this.check();
		} catch (e) {
			throw new AccountNotFoundError(`Unable to get Identifier ${e}`)
		}

		let stored_data = localStorage.getItem('oip_account');

		stored_data = JSON.parse(stored_data);

		if (stored_data[id]) {
			let decrypted_data

			try {
				decrypted_data = this.decrypt(stored_data[id].encrypted_data);
			} catch (e) {
				throw new InvalidPassword("Password is not Valid\n" + e)
			}

			if (decrypted_data) {
				if (!decrypted_data.identifier)
					decrypted_data.identifier = id;

				return decrypted_data
			}
		} else {
			throw new AccountNotFoundError("Account not found for " + id + " in LocalStorage")
		}

		throw new Error("Unable to Decrypt Account!")
	}

	/**
	 * Internal Save function to Save an Account to LocalStorage
	 *
	 * @async
	 * @param  {Object} account_data - The new Account Data you wish to save
	 * @param  {Identifier} identifier - The Identifier of the account you wish to save
	 * @return {Promise<Object>} Returns a Promise that will resolve to the Account Data of the updated account if successful
	 */
	async _save(account_data, identifier) {
		var stored_data = localStorage.getItem('oip_account');

		if (stored_data)
			stored_data = JSON.parse(stored_data);

		if (!stored_data)
			stored_data = {};

		this.encrypt(account_data);

		stored_data[identifier] = {...this.storage, mnemonicHash: hash.update(account_data.wallet.seed).digest('hex')};

		localStorage.setItem('oip_account', JSON.stringify(stored_data));

		return account_data
	}

	/**
	 * Check if the Account exists in LocalStorage.
	 * This matches an email to an identifier if the username being used is an email.
	 *
	 * @async
	 * @return {Promise<Identifier>} Returns a Promsie that will resolve to the Accounts Identifier if set
	 */
	async check() {
		var stored_data = localStorage.getItem('oip_account');

		stored_data = JSON.parse(stored_data);

		if (!stored_data)
			throw new AccountNotFoundError();

		if (this.storage.identifier !== "" && stored_data[this.storage.identifier])
			return this.storage.identifier;

		if (stored_data[this._username])
			return this._username;

		for (let identifier in stored_data) {
			// Check if the Email matches
			if (stored_data[identifier].email && stored_data[identifier].email !== "" && this._username && stored_data[identifier].email === this._username)
				return identifier;

			//toDo: check this code runs
			if (this.seed && stored_data[identifier].mnemonicHash === hash.update(this.seed).digest('hex'))
				return identifier
		}

		//toDo: remove unreachable code if above toDo works
		if (this.seed) {
			try {
				return await this.checkForMnemonic()
			} catch (err) {
				throw err
			}
		}

		throw new AccountNotFoundError()
	}

	/**
	 * Check if the Account exists in LocalStorage by searching for mnemonic in encrypted data.
	 *
	 * @async
	 * @return {Promise<Identifier>} Returns a Promsie that will resolve to the Accounts Identifier if set
	 */
	async checkForMnemonic() {
		let stored_data = localStorage.getItem('oip_account');

		stored_data = JSON.parse(stored_data);

		if (!stored_data)
			throw new AccountNotFoundError();

		for (let identifier in stored_data) {
			let hydrated_decrypted = {}
			try {
				let decrypted_data = CryptoJS.AES.decrypt(stored_data[identifier].encrypted_data, this._password, AES_CONFIG);
				hydrated_decrypted = JSON.parse(decrypted_data.toString(CryptoJS.enc.Utf8));
			} catch (err) {
				throw new InvalidPassword("Unable to decrypt account!\n" + e)
			}


			if (hydrated_decrypted.wallet) {
				if (hydrated_decrypted.wallet.seed === this.seed) {
					return identifier
				}
			}
		}
		throw new AccountNotFoundError(`Unable to find mnemonic`)
	}
}

module.exports = LocalStorageAdapter;