StorageAdapters/StorageAdapter.js

import CryptoJS from 'crypto-js';
import crypto from 'crypto';

import { isValidEmail, isValidIdentifier } from '../util'
import { InvalidPassword } from '../Errors'
import {util} from "oip-hdmw";

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

/**
 * A Unique identifier for the user. This is a set of characters seperated by dashes.
 * @typedef {string} Identifier
 * @example
 * e7c7a45-8aac7317-b2b0098-2c7a046
 */

/**
 * A Generic StorageAdapter class that provides shared functions between all StorageAdapters
 */
class StorageAdapter {
	/**
	 * Create a new Storage Adapter
	 * @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 {StorageAdapter}
	 */
	constructor(username, password){
		this._username = username
		this._password = password || ""

		this.storage = {
			identifier: undefined,
			email: undefined,
			encrypted_data: "",
		};

		if (util.isMnemonic(username)){
			this.seed = username;
			this._username = undefined
		}

		if (this._username && !isValidIdentifier(this._username) && isValidEmail(this._username))
			this.storage.email = this._username;

	}
	/**
	 * Create an account using the StorageAdapter
	 *
	 * @async
	 * @param  {Object} account_data - The Account Data you wish to save
	 * @param  {string} [email]      - The Email you wish to attach to your account
	 * @return {Promise<Identifier>} The Identifier of the Created Account
	 */
	async create(account_data, email){
		var account_data_copy = JSON.parse(JSON.stringify(account_data));

		if (email){
			this.storage.email = email
			account_data_copy.email = email;

			if (!this._username)
				this._username = email
		} else {
			if (isValidEmail(this._username)){
				this.storage.email = this._username
				account_data_copy.email = this._username
			}
		}

		var identifier = this.generateIdentifier();

		account_data_copy.identifier = identifier;

		if (!this._username)
			this._username = identifier

		return await this.save(account_data_copy, identifier)
	}
	/**
	 * Save an Account using the StorageAdapter
	 *
	 * @async
	 * @param  {Object} account_data - The Account Data you wish to save
	 * @param  {Identifier} [identifier] - The Identifier of the Account you wish to save to
	 * @return {Promise<Identifier>} Returns the Identifier of the saved account
	 */
	async save(account_data, identifier){
		if (identifier){
			// Save right away
			return await this._save(account_data, identifier)
		} else {
			// Try to match to an ID
			try {
				// Check if the account already exists
				var id = await this.check()

				// If it already exists, then save the account
				return await this._save(account_data, id)
			} catch(e) {
				// If we have an id, and there is an error, pass the error up
				if (this.storage.identifier && e.response && e.response.data && e.response.data.type)
					throw new Error(e.response.data.type)

				// No ID, generate new and save
				return await this.create(account_data)
			}
		}
	}
	/**
	 * Load the Wallet from the StorageAdapter, this function is overwritten by sub-classes
	 *
	 * @async
	 * @return {Promise<Object>} Returns the Account Data for the specified account
	 */
	async load(){}
	/**
	 * Check if the Wallet exists on the StorageAdapter, this function is overwritten by sub-classes
	 * @return {Promise<Identifier>} Returns the Identifier of the matched wallet (if found)
	 */
	async check(){}
	/**
	 * Generate a valid Identifier
	 * @return {Identifier} Returns a newly generated identifier
	 */
	generateIdentifier() {
		var bytes = crypto.randomBytes(16).toString('hex')

		var identifier = bytes.slice(0, 7) + "-" + bytes.slice(8, 16) + "-" + bytes.slice(17, 24) + "-" + bytes.slice(25, 32)

		this.storage.identifier = identifier;

		return identifier
	}
	/**
	 * Decrypt the Account data
	 * @param  {string} encrypted_data - The Encrypted Data string to Decrypt
	 * @throws {InvalidPassword} If there is an error decrypting the encrypted data using the Password
	 * @return {Object} Returns the decrypted data as a JSON Object
	 */
	decrypt(encrypted_data){
		// Try to decrypt
		try {
			var decrypted = CryptoJS.AES.decrypt(encrypted_data, this._password, AES_CONFIG);
			var hydrated_decrypted = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))

			if (hydrated_decrypted && hydrated_decrypted.email)
				this.storage.email = hydrated_decrypted.email

			if (hydrated_decrypted && hydrated_decrypted.identifier)
				this.storage.identifier = hydrated_decrypted.identifier

			return hydrated_decrypted
		} catch (e) {
			throw new InvalidPassword("Unable to decrypt account!\n" + e)
		}
	}
	/**
	 * Encrypt the Account data
	 * @param  {Object} decrypted_data - A JSON object of the data you would like to encrypt
	 * @return {string} Returns the Encrypted Data as a String
	 */
	encrypt(decrypted_data){
		if (decrypted_data && !decrypted_data.email && this.storage.email && this.storage.email !== "")
			decrypted_data.email = this.storage.email

		if (decrypted_data && !decrypted_data.identifier && this.storage.identifier && this.storage.identifier !== ""){
			decrypted_data.identifier = this.storage.identifier
		}

		try {
			var decrypted_string = JSON.stringify(decrypted_data);
			var encrypted = CryptoJS.AES.encrypt(decrypted_string, this._password, AES_CONFIG)
			var encrypted_string = encrypted.toString();

			this.storage.encrypted_data = encrypted_string;

			return encrypted_string
		} catch (e) {
			console.log('failed to encrypt data: ', e)
			return undefined
		}
	}
}

module.exports = StorageAdapter;