@etsoo/appscript 1.1.64 → 1.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,11 +15,23 @@ import {
15
15
  StorageUtils,
16
16
  Utils
17
17
  } from '@etsoo/shared';
18
- import { AES } from 'crypto-js';
18
+ import {
19
+ AES,
20
+ algo,
21
+ enc,
22
+ HmacSHA512,
23
+ lib,
24
+ mode,
25
+ pad,
26
+ PBKDF2,
27
+ SHA3
28
+ } from 'crypto-js';
19
29
  import { AddressRegion } from '../address/AddressRegion';
20
30
  import { AddressUtils } from '../address/AddressUtils';
31
+ import { InitCallDto } from '../dto/InitCallDto';
21
32
  import { ActionResultError } from '../result/ActionResultError';
22
33
  import { IActionResult } from '../result/IActionResult';
34
+ import { InitCallResult, InitCallResultData } from '../result/InitCallResult';
23
35
  import { IUserData } from '../state/User';
24
36
  import { IAppSettings } from './AppSettings';
25
37
  import { UserRole } from './UserRole';
@@ -130,11 +142,6 @@ export interface ICoreApp<
130
142
  */
131
143
  userData?: IUserData;
132
144
 
133
- /**
134
- * Passphrase for encryption
135
- */
136
- passphrase?: string;
137
-
138
145
  /**
139
146
  * Search input element
140
147
  */
@@ -170,9 +177,14 @@ export interface ICoreApp<
170
177
  * Decrypt message
171
178
  * @param messageEncrypted Encrypted message
172
179
  * @param passphrase Secret passphrase
180
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
173
181
  * @returns Pure text
174
182
  */
175
- decrypt(messageEncrypted: string, passphrase: string): string;
183
+ decrypt(
184
+ messageEncrypted: string,
185
+ passphrase?: string,
186
+ durationSeconds?: number
187
+ ): string | undefined;
176
188
 
177
189
  /**
178
190
  * Detect IP data, call only one time
@@ -184,9 +196,10 @@ export interface ICoreApp<
184
196
  * Encrypt message
185
197
  * @param message Message
186
198
  * @param passphrase Secret passphrase
199
+ * @param iterations Iterations, 1000 times, 1 - 99
187
200
  * @returns Result
188
201
  */
189
- encrypt(message: string, passphrase: string): string;
202
+ encrypt(message: string, passphrase?: string, iterations?: number): string;
190
203
 
191
204
  /**
192
205
  * Format date to string
@@ -290,6 +303,22 @@ export interface ICoreApp<
290
303
  */
291
304
  getTimeZone(): string | undefined;
292
305
 
306
+ /**
307
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
308
+ * https://cryptojs.gitbook.io/docs/
309
+ * @param message Message
310
+ * @param passphrase Secret passphrase
311
+ */
312
+ hash(message: string, passphrase?: string): string;
313
+
314
+ /**
315
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
316
+ * https://cryptojs.gitbook.io/docs/
317
+ * @param message Message
318
+ * @param passphrase Secret passphrase
319
+ */
320
+ hashHex(message: string, passphrase?: string): string;
321
+
293
322
  /**
294
323
  * Check use has the specific role permission or not
295
324
  * @param roles Roles to check
@@ -438,11 +467,6 @@ export abstract class CoreApp<
438
467
  */
439
468
  userData?: IUserData;
440
469
 
441
- /**
442
- * Passphrase for encryption
443
- */
444
- passphrase?: string;
445
-
446
470
  /**
447
471
  * Response token header field name
448
472
  */
@@ -480,6 +504,21 @@ export abstract class CoreApp<
480
504
  */
481
505
  protected refreshCountdownSeed = 0;
482
506
 
507
+ /**
508
+ * Device id field name
509
+ */
510
+ protected deviceIdField: string = 'SmartERPDeviceId';
511
+
512
+ /**
513
+ * Device id
514
+ */
515
+ protected deviceId: string;
516
+
517
+ /**
518
+ * Passphrase for encryption
519
+ */
520
+ protected passphrase: string = '***';
521
+
483
522
  /**
484
523
  * Protected constructor
485
524
  * @param settings Settings
@@ -498,6 +537,11 @@ export abstract class CoreApp<
498
537
  this.notifier = notifier;
499
538
  this.name = name;
500
539
 
540
+ this.deviceId = StorageUtils.getLocalData<string>(
541
+ this.deviceIdField,
542
+ ''
543
+ );
544
+
501
545
  this.setApi(api);
502
546
 
503
547
  const { currentCulture, currentRegion } = settings;
@@ -542,6 +586,108 @@ export abstract class CoreApp<
542
586
  };
543
587
  }
544
588
 
589
+ /**
590
+ * Init call
591
+ * @returns Result
592
+ */
593
+ protected async initCall() {
594
+ const data: InitCallDto = {
595
+ timestamp: new Date().getTime(),
596
+ deviceId: this.deviceId === '' ? undefined : this.deviceId
597
+ };
598
+ const result = await this.api.put<InitCallResult>(
599
+ 'Auth/WebInitCall',
600
+ data
601
+ );
602
+ if (result == null) return;
603
+
604
+ if (result.data == null) {
605
+ this.notifier.alert(this.get<string>('noData')!);
606
+ return;
607
+ }
608
+
609
+ if (!result.ok) {
610
+ const seconds = result.data.seconds;
611
+ const validSeconds = result.data.validSeconds;
612
+ if (
613
+ result.title === 'timeDifferenceInvalid' &&
614
+ seconds != null &&
615
+ validSeconds != null
616
+ ) {
617
+ const title = this.get('timeDifferenceInvalid')?.format(
618
+ seconds.toString(),
619
+ validSeconds.toString()
620
+ );
621
+ this.notifier.alert(title!);
622
+ } else {
623
+ this.alertResult(result);
624
+ }
625
+
626
+ return;
627
+ }
628
+
629
+ this.initCallUpdate(result.data, data.timestamp);
630
+ }
631
+
632
+ /**
633
+ * Init call update
634
+ * @param data Result data
635
+ * @param timestamp Timestamp
636
+ */
637
+ protected initCallUpdate(data: InitCallResultData, timestamp: number) {
638
+ if (data.deviceId == null || data.passphrase == null) return;
639
+
640
+ // Decrypt
641
+ // Should be done within 120 seconds after returning from the backend
642
+ const passphrase = this.decrypt(
643
+ data.passphrase,
644
+ timestamp.toString(),
645
+ 120
646
+ );
647
+ if (passphrase == null) return;
648
+
649
+ // Update device id and cache it
650
+ this.deviceId = data.deviceId;
651
+ StorageUtils.setLocalData(this.deviceIdField, this.deviceId);
652
+
653
+ // Current passphrase
654
+ this.passphrase = passphrase;
655
+
656
+ // Previous passphrase
657
+ if (data.previousPassphrase) {
658
+ const prev = this.decrypt(
659
+ data.previousPassphrase,
660
+ timestamp.toString(),
661
+ 120
662
+ );
663
+
664
+ // Update
665
+ const fields = this.initCallUpdateFields();
666
+ for (const field of fields) {
667
+ const currentValue = StorageUtils.getLocalData<string>(
668
+ field,
669
+ ''
670
+ );
671
+ if (currentValue === '' || currentValue.indexOf('+') === -1)
672
+ continue;
673
+
674
+ const newValueSource = this.decrypt(currentValue, prev, 12);
675
+ if (newValueSource == null) continue;
676
+
677
+ const newValue = this.encrypt(newValueSource);
678
+ StorageUtils.setLocalData(field, newValue);
679
+ }
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Init call update fields in local storage
685
+ * @returns Fields
686
+ */
687
+ protected initCallUpdateFields(): string[] {
688
+ return [];
689
+ }
690
+
545
691
  /**
546
692
  * Alert action result
547
693
  * @param result Action result
@@ -657,16 +803,57 @@ export abstract class CoreApp<
657
803
  * Decrypt message
658
804
  * @param messageEncrypted Encrypted message
659
805
  * @param passphrase Secret passphrase
806
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
660
807
  * @returns Pure text
661
808
  */
662
- decrypt(messageEncrypted: string, passphrase: string) {
809
+ decrypt(
810
+ messageEncrypted: string,
811
+ passphrase?: string,
812
+ durationSeconds?: number
813
+ ) {
814
+ // Timestamp splitter
663
815
  const pos = messageEncrypted.indexOf('+');
664
- const miliseconds = messageEncrypted.substring(0, pos);
816
+ if (pos === -1 || messageEncrypted.length <= 66) return undefined;
817
+
818
+ const timestamp = messageEncrypted.substring(0, pos);
665
819
  const message = messageEncrypted.substring(pos + 1);
666
- return AES.decrypt(
667
- message,
668
- this.encryptionEnhance(passphrase, miliseconds)
669
- ).toString();
820
+
821
+ if (durationSeconds != null && durationSeconds > 0) {
822
+ const milseconds = Utils.charsToNumber(timestamp);
823
+ if (isNaN(milseconds) || milseconds < 1) return undefined;
824
+ const timespan = new Date().substract(new Date(milseconds));
825
+ if (
826
+ (durationSeconds <= 12 &&
827
+ timespan.totalMonths > durationSeconds) ||
828
+ (durationSeconds > 12 &&
829
+ timespan.totalSeconds > durationSeconds)
830
+ )
831
+ return undefined;
832
+ }
833
+
834
+ // Iterations
835
+ const iterations = parseInt(message.substring(0, 2), 10);
836
+ if (isNaN(iterations)) return undefined;
837
+
838
+ const salt = enc.Hex.parse(message.substring(2, 34));
839
+ const iv = enc.Hex.parse(message.substring(34, 66));
840
+ const encrypted = message.substring(66);
841
+
842
+ const key = PBKDF2(
843
+ this.encryptionEnhance(passphrase ?? this.passphrase, timestamp),
844
+ salt,
845
+ {
846
+ keySize: 8, // 256 / 32
847
+ hasher: algo.SHA256,
848
+ iterations: 1000 * iterations
849
+ }
850
+ );
851
+
852
+ return AES.decrypt(encrypted, key, {
853
+ iv,
854
+ padding: pad.Pkcs7,
855
+ mode: mode.CBC
856
+ }).toString(enc.Utf8);
670
857
  }
671
858
 
672
859
  /**
@@ -713,30 +900,53 @@ export abstract class CoreApp<
713
900
  * Encrypt message
714
901
  * @param message Message
715
902
  * @param passphrase Secret passphrase
903
+ * @param iterations Iterations, 1000 times, 1 - 99
716
904
  * @returns Result
717
905
  */
718
- encrypt(message: string, passphrase: string) {
719
- const miliseconds = Utils.numberToChars(new Date().getTime());
906
+ encrypt(message: string, passphrase?: string, iterations?: number) {
907
+ // Default 1 * 1000
908
+ iterations ??= 1;
909
+
910
+ // Timestamp
911
+ const timestamp = Utils.numberToChars(new Date().getTime());
912
+
913
+ const bits = 16; // 128 / 8
914
+ const salt = lib.WordArray.random(bits);
915
+ const key = PBKDF2(
916
+ this.encryptionEnhance(passphrase ?? this.passphrase, timestamp),
917
+ salt,
918
+ {
919
+ keySize: 8, // 256 / 32
920
+ hasher: algo.SHA256,
921
+ iterations: 1000 * iterations
922
+ }
923
+ );
924
+ const iv = lib.WordArray.random(bits);
925
+
720
926
  return (
721
- miliseconds +
927
+ timestamp +
722
928
  '+' +
723
- AES.encrypt(
724
- message,
725
- this.encryptionEnhance(passphrase, miliseconds)
726
- ).toString()
929
+ iterations.toString().padStart(2, '0') +
930
+ salt.toString(enc.Hex) +
931
+ iv.toString(enc.Hex) +
932
+ AES.encrypt(message, key, {
933
+ iv,
934
+ padding: pad.Pkcs7,
935
+ mode: mode.CBC
936
+ }).toString() // enc.Base64
727
937
  );
728
938
  }
729
939
 
730
940
  /**
731
941
  * Enchance secret passphrase
732
942
  * @param passphrase Secret passphrase
733
- * @param miliseconds Miliseconds
943
+ * @param timestamp Timestamp
734
944
  * @returns Enhanced passphrase
735
945
  */
736
- protected encryptionEnhance(passphrase: string, miliseconds: string) {
737
- passphrase += miliseconds;
946
+ protected encryptionEnhance(passphrase: string, timestamp: string) {
947
+ passphrase += timestamp;
738
948
  passphrase += passphrase.length.toString();
739
- return passphrase + (this.passphrase ?? '');
949
+ return passphrase;
740
950
  }
741
951
 
742
952
  /**
@@ -818,7 +1028,18 @@ export abstract class CoreApp<
818
1028
  * @param forceToLocal Force to local labels
819
1029
  */
820
1030
  formatResult(result: IActionResult, forceToLocal?: boolean) {
821
- if ((result.title == null || forceToLocal) && result.type != null) {
1031
+ const title = result.title;
1032
+ if (title && /^\w+$/.test(title)) {
1033
+ const key = title.formatInitial(false);
1034
+ const localTitle = this.get(key);
1035
+ if (localTitle) {
1036
+ result.title = localTitle;
1037
+
1038
+ // Hold the original title in type when type is null
1039
+ if (result.type == null) result.type = title;
1040
+ }
1041
+ } else if ((title == null || forceToLocal) && result.type != null) {
1042
+ // Get label from type
822
1043
  const key = result.type.formatInitial(false);
823
1044
  result.title = this.get(key);
824
1045
  }
@@ -856,7 +1077,10 @@ export abstract class CoreApp<
856
1077
  * @returns Cached token
857
1078
  */
858
1079
  getCacheToken(): string | null {
859
- let refreshToken = StorageUtils.getLocalData(this.headerTokenField, '');
1080
+ let refreshToken = StorageUtils.getLocalData<string>(
1081
+ this.headerTokenField,
1082
+ ''
1083
+ );
860
1084
  if (refreshToken === '')
861
1085
  refreshToken = StorageUtils.getSessionData(
862
1086
  this.headerTokenField,
@@ -897,6 +1121,30 @@ export abstract class CoreApp<
897
1121
  return this.settings.timeZone ?? this.ipData?.timezone;
898
1122
  }
899
1123
 
1124
+ /**
1125
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
1126
+ * https://cryptojs.gitbook.io/docs/
1127
+ * @param message Message
1128
+ * @param passphrase Secret passphrase
1129
+ */
1130
+ hash(message: string, passphrase?: string) {
1131
+ if (passphrase == null)
1132
+ return SHA3(message, { outputLength: 512 }).toString(enc.Base64);
1133
+ else return HmacSHA512(message, passphrase).toString(enc.Base64);
1134
+ }
1135
+
1136
+ /**
1137
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
1138
+ * https://cryptojs.gitbook.io/docs/
1139
+ * @param message Message
1140
+ * @param passphrase Secret passphrase
1141
+ */
1142
+ hashHex(message: string, passphrase?: string) {
1143
+ if (passphrase == null)
1144
+ return SHA3(message, { outputLength: 512 }).toString(enc.Hex);
1145
+ else return HmacSHA512(message, passphrase).toString(enc.Hex);
1146
+ }
1147
+
900
1148
  /**
901
1149
  * Check use has the specific role permission or not
902
1150
  * @param roles Roles to check
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Init call dto
3
+ */
4
+ export type InitCallDto = {
5
+ /**
6
+ * Device id
7
+ */
8
+ deviceId?: string;
9
+
10
+ /**
11
+ * Timestamp
12
+ */
13
+ timestamp: number;
14
+ };
@@ -59,6 +59,7 @@
59
59
  "status": "Status",
60
60
  "submit": "Submit",
61
61
  "success": "Success",
62
+ "timeDifferenceInvalid": "The time difference between the device and the server is {0}, which exceeds the limit of {1} seconds. Please adjust the device time. If it is abnormal, please inform the administrator",
62
63
  "tokenExpiry": "Your session is about to expire. Click the Cancel button to continue",
63
64
  "yes": "Yes",
64
65
  "unknownError": "Unknown Error",
@@ -59,6 +59,7 @@
59
59
  "status": "状态",
60
60
  "submit": "提交",
61
61
  "success": "成功",
62
+ "timeDifferenceInvalid": "设备时间和服务器时间差为{0},超过{1}秒的限制,请调整设备时间,如果异常请告知管理员",
62
63
  "tokenExpiry": "您的会话即将过期。点击 取消 按钮继续使用",
63
64
  "yes": "是",
64
65
  "unknownError": "未知错误",
@@ -59,6 +59,7 @@
59
59
  "status": "狀態",
60
60
  "submit": "提交",
61
61
  "success": "成功",
62
+ "timeDifferenceInvalid": "設備時間和服務器時間差為{0},超過{1}秒的限制,請調整設備時間,如果異常請告知管理員",
62
63
  "tokenExpiry": "您的會話即將過期。點擊 取消 按鈕繼續使用",
63
64
  "yes": "是",
64
65
  "unknownError": "未知錯誤",
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export * from './def/ListItem';
27
27
  export * from './dto/IdDto';
28
28
  export * from './dto/IdLabelDto';
29
29
  export * from './dto/IdLabelPrimaryDto';
30
+ export * from './dto/InitCallDto';
30
31
  export * from './dto/UpdateDto';
31
32
 
32
33
  // i18n
@@ -42,6 +43,7 @@ export type { IApi, IApiPayload } from '@etsoo/restclient';
42
43
  export * from './result/ActionResult';
43
44
  export * from './result/ActionResultError';
44
45
  export * from './result/IActionResult';
46
+ export * from './result/InitCallResult';
45
47
 
46
48
  // state
47
49
  export * from './state/Culture';
@@ -56,12 +56,12 @@ export interface IActionResult<D extends IResultData = IResultData> {
56
56
  /**
57
57
  * Trace id
58
58
  */
59
- readonly traceId?: string;
59
+ traceId?: string;
60
60
 
61
61
  /**
62
62
  * Type
63
63
  */
64
- readonly type: string;
64
+ type: string;
65
65
 
66
66
  /**
67
67
  * Success or not
@@ -0,0 +1,36 @@
1
+ import { IActionResult, IResultData } from './IActionResult';
2
+
3
+ /**
4
+ * Init call result data
5
+ */
6
+ export interface InitCallResultData extends IResultData {
7
+ /**
8
+ * Device id
9
+ */
10
+ deviceId?: string;
11
+
12
+ /**
13
+ * Secret passphrase
14
+ */
15
+ passphrase?: string;
16
+
17
+ /**
18
+ * Previous secret passphrase
19
+ */
20
+ previousPassphrase?: string;
21
+
22
+ /**
23
+ * Actual seconds gap
24
+ */
25
+ seconds?: number;
26
+
27
+ /**
28
+ * Valid seconds gap
29
+ */
30
+ validSeconds?: number;
31
+ }
32
+
33
+ /**
34
+ * Init call result
35
+ */
36
+ export type InitCallResult = IActionResult<InitCallResultData>;