ngneat / elf

🧙‍♀️ A Reactive Store with Magical Powers

Home Page:https://ngneat.github.io/elf/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`preStoreInit` doesn't behave as expected

micobarac opened this issue · comments

I am using preStoreInit and preStorageUpdate for symmetric encryption of local storage store data.

import { Settings } from '@app/app.settings';
import CryptoJS from 'crypto-js';

export class CryptUtil {
  static readonly Key: string = Settings.Key;

  static encrypt(value: any): any {
    return CryptoJS.AES.encrypt(JSON.stringify(value), CryptUtil.Key).toString();
  }

  static decrypt(encryptedValue: any): any {
    const bytes = CryptoJS.AES.decrypt(encryptedValue, CryptUtil.Key);
    return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
  }
}
import { Injectable } from '@angular/core';
import { Settings } from '@app/app.settings';
import { User } from '@content/administration/user/user.model';
import { StoreValue, createStore, select, withProps } from '@ngneat/elf';
import { localStorageStrategy, persistState } from '@ngneat/elf-persist-state';
import { CryptUtil } from '@shared/helpers/crypt.util';

const name: string = Settings.Prefix + 'account';

interface AccountProps {
  user: User | null;
}

const accountStore = createStore({ name }, withProps<AccountProps>({ user: null }));

const preStoreInit = (state: StoreValue<any>) => {
  return CryptUtil.decrypt(state);
};

const preStorageUpdate = (storeName: string, state: Partial<AccountProps>) => {
  return CryptUtil.encrypt(state);
};

export const persist = persistState(accountStore, {
  key: name,
  storage: localStorageStrategy,
  preStoreInit,
  preStorageUpdate
});

@Injectable({
  providedIn: 'root'
})
export class AccountRepository {
  user$ = accountStore.pipe(select((state) => state.user));

  setAccount(user: User) {
    accountStore.update((state) => ({ ...state, user }));
  }

  reset() {
    accountStore.reset();
  }
}

This is the encrypted value written in local storage:

"U2FsdGVkX1+o9DoUgu7oH0Wkzo5u0hXGrYr39oFJ5gvZ9hZ8GoHuJN8TBBiuKzJkZjmi02mX+dtCs+rJpqIEac5FAwQ9O/3QawbfC21tVVRA657eLhPWDC02mR++UMCFfZySzkvkZBZlLMYp+XnH+5mXrecT/moIcewfHa9AtcQxnmEzd8cKcZK2D/XFlQI1cxQHJAg5jot/Vp8X+kL36MF/qcPuE+TuZVj9a1E3G6vLmTEvhLKaydw/40/n2pUxHbGG/5AdeCp7aFH0OQD2UgepD6bxDcIk5oC7jtWWwXlGI0sgKtK8ecKX757DIasgBddq8P9P+UZL930GNk0u7UQQp4wJNkYwgIKp1AtcgjOkxE4LgVuIKHQjDTBDulRzsSumJDGL75hSq+1ga4WwpjXPkmubs2pHfNsPjmCkPEeiEyE1bAdHlpWpvjuI7vaBhtF/3+DT/etwXTgHXvkAOf0BTDDDKyXm04jzLhSBS5zTQ5zlKiGpg8NRYADYzfVqmd4Yu4B6dIHmWevJngZZP8DkaL+hRxFKpJuQ4H7feybtN6m8UvPgawfwqbh0zp+1g7CvfZpzARAogV+AYC0gs2Hkmx/mOndTKKV0BRV7B4lzn2SnMowa2tBYcUxWq63Nh0JhMLsmwNEOl616vYohaSoj1+iZ7dMIhxAvrw1YuqXxXslR653emQ0fwWWSYf5HLbRc9Dx18JF/WzLMn5yrOjaZJpZLYQUs+vwHOV7WMkDpQyFGMFoC9tGeB0GDeG9Me0o9zN8OESe3OoD68wEaEMXHgZPUo9ynv85swTwQ9I7IW7+pLu3Nz2mip6ZGopx5JNbzbgXRPmuXMKKqNkn3us2kJ0SxJ6sNx/2Pf4dZ5Ts9+/THN8rFGLDoEmcbt15KytgXwTOXk8j7hIh4mthzZkkMSGXTFTqoRimbHBUSGSru0k3/vvg7eX3Vjb/fangrGrZ9WgUbS4lpnkLHCaHCCWlOm3mXNtB7iv8qa69a974FQDFlGMEvy05vYKcSh3CQQL1HB+70RKfmi90KDOeuksYDVDpFx8/J4shlGaLiW32kNbc6q8K+MfDRKtzW4C+xShjIHsE9f3et2ButxBnNeGhwCEWlq90GSijsAWTgiTh5PhidJpHSLVwmV6g3BDdXHvscRqo6CCAIRoBXbcpFntpRnz83UNX7+WwxMXmzXkljXPT+jt1ULaAMfuyVQNYrgdwB/aMvln9PutqO4pZ4oPTm4WA4ZfOtFZ/q5p8mZnZscNUptW5Tj/djLSZss97BvmStV9LAJS6jdoHGGykjeb/YGULnQxHUMAOXy27X/3KI2lb2aR8HD/1DMsdmMcLwSSdkKP9zZST59ginPUnw9/EVZj2bL54LKfiXAFsxwaUxOlGtSJSuGbzHvRPtZH/xfhDabnY5eWSsJUtsH7LLZR52BBSLyRLOaNI5QaKbBdaLNIVaqLjEFIM3RuW3rYc5HsmTxAdXYOk+0rnlUPfLE/+d5QkY5+nH/c0M+E0oQp6sJlwglFfwGQpoph5R3IadNom/8OSkypg42+np5GWYSEXjgonmCSemceG4HpsrxjtlBdISEpBM5vurm56zW4nvpaGwxhAgmOxF0c8E/6da6Doabxj6ygISggoXQnUOYcIFxWl74EaIiywm9l19Ib+WdNVd1tRTz6lz2+yvl4j47fC0Q0rVpNManlz+8i9B7xFpqBSmKblCfl4kSMe4RQ67y/CQNcVgpx2k0ozFuuzqi6+wwvi0JCbtnFXVR5TsJJTEp2qqKlWcczp5qnxUeZqHg/7O9ngbIwe/v2QkK0MR2dZr2alzo0k1eyazobeupPWEKz8qxvJzzzGS7zLjr+bNYitUtHOyKaqNxogmdv1G2nhm3Hg1iXt/FUVyFOYQM3FOP4Z1Sz+zXCrWXzVya+YJczbSSO/yLBavjhwEPZl8pghpszxnY3guAIBn7Ngs4zwtjuqwcohsItOiVgt+uuvMKIuRS7mrGLOcDaJ0w7UudOVuZ2LlmWBsKkeDvysY6Q4KfTbk9IYCI/x4Ie6hU+1vvCmqB3gGVgBt3thuLZK1pXyfpvS3tr5kfAQW4u+sa1QZYpl/YSUSLWph0AXcxKKHlFy1bam9cSQSmBWPWW56OiObW5L8/pENy2lO+kZrIKsrG3FpTORl3CzF23OkZeM5FeL8gnomrc1GLfdZ/6tO8SVpXyS4mm4ftOcywdRdceqIDUFLIptj8wkqMSQjH9AHp78haNotLlLYJxxa1D0J0kBd8xfn54DLPvuqckECv0JzF3uIIrEd8gSSBrfFH+7AIn9WDmKQ3A8VvlKtHQIZiAjpZyNi2bNNpT7lHZvvWoDCnEddDPey4hem+v56vVJ6i6nNb1PKjlaEnDlfTYdtZ45Kc439DO7qlYd/tEdKHCxG1ZGa4f54yJNVidO15QTvyZdN+ZhUBIY9P82J77jPw9trvo1vs4dDfcdDV/86BxoQJLG/l3nVmHtat2q1GlOfJUYD5ToI+djB3S6OK4qDg6B9A18DuxcuKsMP3hh0ELM6AEPwV7frF40NfTP/danvCs7YgZJuPC9Cm7Gnu34J45TcFUSTpP+HeTsh/D8wkPE7ckoY2lHYBjaUYUUirt2Uh/TUMJQV5eLBoistvQi+EsyfH/n0A6qXUaHsCLZ94xGTP3zAqoMjgVCJvl71O8du8B1MU/KT1DZ12gddm0E6X3y1NabtkMxf3ryuovnYIk7S9sUDZnUMLh7abCBfD2oCkpbC6j7HVjsxyr7IJXGAHkB0VQpkzSK3QjcBcYSFfoZTYm6Vp2KQzWDqivB5KS3qoQzq8DSRKyhfALrl1z3tUykzqqJXtGOOAt1FQ82XOWfV5gQTDTJr2ht419BPjKqXnSscj/aGCCfkgwwlzonXGzc6hLXS6+Sx+7eORy58XrDbMjliywa4I1HZigFYd7wFPnLEhXiIcHSNYYX3ASm18Np9EMSVtw20qpt9SAlAERK6zzaK10HpJ+7bZFENz9iviuOHYIupfkBmJ2sSrof4hbV8Pa7Cp87U/ibKFZCs2Ut+/8yfk+k0CV7X4HkPg+OvG28gCBgzJI0w172h0aXsvZs00+1Y0RjsEKi4X/zXhGplNjxemVHtECakCk69UPwkS0OUv3wvV58km5AkP2EPG2VpFe+slM5PAJHHNEgyZbNMRR7LpY3bPGvoTOuC9Q7PGLJXCAGUoafOIiltWBW08Jir3ybSLoDuNLj6yqkeLIffhsEesezM2v4WcDxHWIXiLUpP+ciFAXkgJbX50+M33ztKzKN8uS1gU2KykbFeKw9o0xMeiZcWU1tQiIpY+PvhFtL9gwdHuWP6PYTcjV5dCE11nZTvr/ARPfHc2/VuDA3PuEXPLe4cgSlxrVMs/rOPZct4tEAWgYCmdRAd7BD9iyIN/NNDbKD9DbsMZRRXF5rdUGbvvLBdTTw/OXMorshAlQq6m+vpMsoraBykpWGEBiqwVmhdbQJ4WK9Etx/G2MSZ8XvWb+vwK0YY5EVzFxshzj1dXYuGIoYnl+ONBJT9L2/pgCWDGioPN+5fiafVHAW58bdelsvUV829a3AFEeuhpkZmaZElbRYPjOSlnCW4ZTpO0LyrX2Ghcr8Qw7C7YI1iSBCOcZ+r3j+CDTZJcrjVuvqHiXS6xs+Tc0VBw6sS0Ri1ukXeN+BY4Uas+wpN0IU5p/V5qnxpyrcbves50SOGNwVPGQnrjPMO4oKiFc8uHL3tmFSSfiS05D9hi1t/oLdPi51i87/LxoI9fQNBovIs06fdft..."

The problem is, when I try to decrypt, using preStoreInit, the local storage string value is converted into an object, making it impossible to decrypt the value back:

{"0":"U","1":"2","2":"F","3":"s","4":"d","5":"G","6":"V","7":"k","8":"X","9":"1","10":"+","11":"o","12":"9","13":"D","14":"o","15":"U","16":"g","17":"u","18":"7","19":"o","20":"H","21":"0","22":"W","23":"k","24":"z","25":"o","26":"5","27":"u","28":"0","29":"h","30":"X","31":"G","32":"r","33":"Y","34":"r","35":"3","36":"9","37":"o","38":"F","39":"J","40":"5","41":"g","42":"v","43":"Z","44":"9","45":"h","46":"Z","47":"8","48":"G","49":"o","50":"H","51":"u","52":"J","53":"N","54":"8","55":"T","56":"B","57":"B","58":"i","59":"u","60":"K","61":"z","62":"J","63":"k","64":"Z","65":"j","66":"m","67":"i","68":"0","69":"2","70":"m","71":"X","72":"+","73":"d","74":"t","75":"C","76":"s","77":"+","78":"r","79":"J","80":"p","81":"q","82":"I","83":"E","84":"a","85":"c","86":"5","87":"F","88":"A","89":"w","90":"Q","91":"9","92":"O","93":"/","94":"3","95":"Q","96":"a","97":"w","98":"b","99":"f","100":"C","101":"2","102":"1","103":"t","104":"V","105":"V","106":"R","107":"A","108":"6","109":"5","110":"7","111":"e","112":"L","113":"h","114":"P","115":"W","116":"D","117":"C","118":"0","119":"2","120":"m","121":"R","122":"+","123":"+","124":"U","125":"M","126":"C","127":"F","128":"f","129":"Z","130":"y","131":"S","132":"z","133":"k","134":"v","135":"k","136":"Z","137":"B","138":"Z","139":"l","140":"L","141":"M","142":"Y","143":"p","144":"+","145":"X","146":"n","147":"H","148":"+","149":"5","150":"m"...,"user":null}

Btw, do the the following methods declarations support the scenario (StoreValue<S>)?:

preStoreInit?: (value: StoreValue<S>) => Partial<StoreValue<S>>;
preStorageUpdate?: (storeName: string, state: Partial<StoreValue<S>>) => Partial<StoreValue<S>>;

A store should contain only objects so it's OK. You can stringily it and then decrypt.

If I do:

  static decrypt(encryptedValue: string): any {
    const bytes = CryptoJS.AES.decrypt(JSON.stringify(encryptedValue), CryptUtil.Key);
    return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
  }

I get Malformed UTF-8 data.

The stored encryptedValue should be passed, in that case, in its original format (type), for the encryption/decryption to work. It looks as though the stored local storage value gets formatted into object like Object.assign({}, account), thus, passing preStoreInit object, instead of encrypted string.

So, maybe, the crucial declarations might look like this, to support this scenario:

preStoreInit?: (value: StoreValue<S> | string) => Partial<StoreValue<S>>;
preStorageUpdate?: (storeName: string, state: Partial<StoreValue<S>>) => Partial<StoreValue<S>> | string;

If it's an object, convert it to store value, otherwise, allow these methods' implementation to do the conversion.

I managed to get it working like this, but it's a hacky and ugly solution:

const preStoreInit = (state: StoreValue<any>) => {
  const { user, ...encryptedValue } = state;
  const value = Object.values(encryptedValue).join('');
  return CryptUtil.decrypt(value);
};

Maybe, changing persist-state.ts like this can allow primitive string values from local storage:

  import { isObject } from '../store/utils';

  const loadFromStorageSubscription = from(
    storage.getItem(merged.key!)
  ).subscribe((value) => {
    if (value) {
      store.update((state) => {
        return merged.preStoreInit!({
          ...state,
          isObject(value)
            ? {
                ...value,
              }
            : value,
        });
      });
    }

    initialized.next(true);
    initialized.complete();
  });

You're welcome to create a PR that accepts preStoreInitValueParser option