@bantis/local-cipher 2.2.0 → 2.3.0
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.
- package/README.md +85 -2
- package/dist/angular/StorageService.d.ts +22 -0
- package/dist/angular.d.ts +1 -0
- package/dist/angular.esm.js +506 -29
- package/dist/angular.esm.js.map +1 -1
- package/dist/angular.js +517 -28
- package/dist/angular.js.map +1 -1
- package/dist/core/EncryptionHelper.d.ts +1 -0
- package/dist/core/SecureCookie.d.ts +41 -0
- package/dist/core/SecureStorage.d.ts +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.esm.js +462 -29
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +472 -28
- package/dist/index.js.map +1 -1
- package/dist/managers/CookieManager.d.ts +17 -0
- package/dist/managers/PlainCookie.d.ts +14 -0
- package/dist/managers/PlainStorage.d.ts +13 -0
- package/dist/managers/StorageManager.d.ts +17 -0
- package/dist/managers/index.d.ts +17 -0
- package/dist/react/hooks.d.ts +12 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.esm.js +487 -37
- package/dist/react.esm.js.map +1 -1
- package/dist/react.js +498 -37
- package/dist/react.js.map +1 -1
- package/dist/types/index.d.ts +36 -0
- package/package.json +2 -2
package/dist/react.js
CHANGED
|
@@ -21,6 +21,9 @@ const DEFAULT_CONFIG = {
|
|
|
21
21
|
compressionThreshold: 1024,
|
|
22
22
|
autoCleanup: true,
|
|
23
23
|
cleanupInterval: 60000,
|
|
24
|
+
enableCache: true,
|
|
25
|
+
verifyIntegrityOnRead: false,
|
|
26
|
+
storageEngine: typeof window !== 'undefined' ? window.localStorage : {},
|
|
24
27
|
},
|
|
25
28
|
debug: {
|
|
26
29
|
enabled: false,
|
|
@@ -48,6 +51,8 @@ class EncryptionHelper {
|
|
|
48
51
|
this.baseKey = '';
|
|
49
52
|
this.baseKeyPromise = null;
|
|
50
53
|
this.keyVersion = 1;
|
|
54
|
+
// Cache para optimización de rendimiento
|
|
55
|
+
this.keyNameCache = new Map();
|
|
51
56
|
this.config = { ...DEFAULT_CONFIG.encryption, ...config };
|
|
52
57
|
// Load key version from storage
|
|
53
58
|
const storedVersion = localStorage.getItem(EncryptionHelper.KEY_VERSION_KEY);
|
|
@@ -217,10 +222,16 @@ class EncryptionHelper {
|
|
|
217
222
|
* @returns Nombre encriptado con prefijo __enc_
|
|
218
223
|
*/
|
|
219
224
|
async encryptKey(keyName) {
|
|
225
|
+
// Optimización: devolver desde cache si existe
|
|
226
|
+
if (this.keyNameCache.has(keyName)) {
|
|
227
|
+
return this.keyNameCache.get(keyName);
|
|
228
|
+
}
|
|
220
229
|
const baseKey = await this.generateBaseKey();
|
|
221
230
|
const combined = keyName + baseKey;
|
|
222
231
|
const hash = await this.hashString(combined);
|
|
223
|
-
|
|
232
|
+
const encryptedKey = `__enc_${hash.substring(0, 16)}`;
|
|
233
|
+
this.keyNameCache.set(keyName, encryptedKey);
|
|
234
|
+
return encryptedKey;
|
|
224
235
|
}
|
|
225
236
|
/**
|
|
226
237
|
* Limpia todos los datos encriptados del localStorage
|
|
@@ -238,10 +249,11 @@ class EncryptionHelper {
|
|
|
238
249
|
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
239
250
|
// Eliminar salt
|
|
240
251
|
localStorage.removeItem(EncryptionHelper.SALT_STORAGE_KEY);
|
|
241
|
-
// Resetear clave en memoria
|
|
252
|
+
// Resetear clave en memoria y cache
|
|
242
253
|
this.key = null;
|
|
243
254
|
this.baseKey = '';
|
|
244
255
|
this.baseKeyPromise = null;
|
|
256
|
+
this.keyNameCache.clear();
|
|
245
257
|
}
|
|
246
258
|
/**
|
|
247
259
|
* Verifica si el navegador soporta Web Crypto API
|
|
@@ -681,6 +693,7 @@ function shouldCompress(data, threshold = 1024) {
|
|
|
681
693
|
class SecureStorage {
|
|
682
694
|
constructor(config) {
|
|
683
695
|
this.cleanupInterval = null;
|
|
696
|
+
this.memoryCache = new Map();
|
|
684
697
|
// Merge config with defaults
|
|
685
698
|
this.config = {
|
|
686
699
|
encryption: { ...DEFAULT_CONFIG.encryption, ...config?.encryption },
|
|
@@ -762,8 +775,12 @@ class SecureStorage {
|
|
|
762
775
|
const encryptedKey = await this.encryptionHelper.encryptKey(key);
|
|
763
776
|
// Encrypt the value
|
|
764
777
|
const encryptedValue = await this.encryptionHelper.encrypt(serialized);
|
|
765
|
-
// Store in
|
|
766
|
-
|
|
778
|
+
// Store in storage
|
|
779
|
+
this.config.storage.storageEngine.setItem(encryptedKey, encryptedValue);
|
|
780
|
+
// Update cache
|
|
781
|
+
if (this.config.storage.enableCache) {
|
|
782
|
+
this.memoryCache.set(key, { value });
|
|
783
|
+
}
|
|
767
784
|
this.logger.verbose(`Stored key: ${key}, compressed: ${compressed}, size: ${encryptedValue.length}`);
|
|
768
785
|
this.eventEmitter.emit('encrypted', { key, metadata: { compressed, size: encryptedValue.length } });
|
|
769
786
|
this.logger.timeEnd(`setItem:${key}`);
|
|
@@ -771,7 +788,7 @@ class SecureStorage {
|
|
|
771
788
|
catch (error) {
|
|
772
789
|
this.logger.error(`Error al guardar dato encriptado para ${key}:`, error);
|
|
773
790
|
this.eventEmitter.emit('error', { key, error: error });
|
|
774
|
-
|
|
791
|
+
this.config.storage.storageEngine.setItem(key, value);
|
|
775
792
|
}
|
|
776
793
|
}
|
|
777
794
|
/**
|
|
@@ -824,8 +841,12 @@ class SecureStorage {
|
|
|
824
841
|
const encryptedKey = await this.encryptionHelper.encryptKey(key);
|
|
825
842
|
// Encrypt the value
|
|
826
843
|
const encryptedValue = await this.encryptionHelper.encrypt(serialized);
|
|
827
|
-
// Store in
|
|
828
|
-
|
|
844
|
+
// Store in storage
|
|
845
|
+
this.config.storage.storageEngine.setItem(encryptedKey, encryptedValue);
|
|
846
|
+
// Update cache
|
|
847
|
+
if (this.config.storage.enableCache) {
|
|
848
|
+
this.memoryCache.set(key, { value, expiresAt });
|
|
849
|
+
}
|
|
829
850
|
this.logger.verbose(`Stored key with expiry: ${key}, expiresAt: ${expiresAt}`);
|
|
830
851
|
this.eventEmitter.emit('encrypted', { key, metadata: { compressed, expiresAt } });
|
|
831
852
|
this.logger.timeEnd(`setItemWithExpiry:${key}`);
|
|
@@ -842,16 +863,31 @@ class SecureStorage {
|
|
|
842
863
|
async getItem(key) {
|
|
843
864
|
this.logger.time(`getItem:${key}`);
|
|
844
865
|
if (!EncryptionHelper.isSupported()) {
|
|
845
|
-
return
|
|
866
|
+
return this.config.storage.storageEngine.getItem(key);
|
|
846
867
|
}
|
|
847
868
|
try {
|
|
869
|
+
// Check memory cache first
|
|
870
|
+
if (this.config.storage.enableCache && this.memoryCache.has(key)) {
|
|
871
|
+
const cached = this.memoryCache.get(key);
|
|
872
|
+
if (cached.expiresAt && cached.expiresAt < Date.now()) {
|
|
873
|
+
this.logger.info(`Key expired in cache: ${key}`);
|
|
874
|
+
await this.removeItem(key);
|
|
875
|
+
this.eventEmitter.emit('expired', { key });
|
|
876
|
+
this.logger.timeEnd(`getItem:${key}`);
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
this.logger.debug(`Retrieved from cache: ${key}`);
|
|
880
|
+
this.eventEmitter.emit('decrypted', { key });
|
|
881
|
+
this.logger.timeEnd(`getItem:${key}`);
|
|
882
|
+
return cached.value;
|
|
883
|
+
}
|
|
848
884
|
// Encrypt the key
|
|
849
885
|
const encryptedKey = await this.encryptionHelper.encryptKey(key);
|
|
850
886
|
// Get encrypted value
|
|
851
|
-
let encryptedValue =
|
|
887
|
+
let encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
|
|
852
888
|
// Backward compatibility: try with plain key
|
|
853
889
|
if (!encryptedValue) {
|
|
854
|
-
encryptedValue =
|
|
890
|
+
encryptedValue = this.config.storage.storageEngine.getItem(key);
|
|
855
891
|
if (!encryptedValue) {
|
|
856
892
|
this.logger.timeEnd(`getItem:${key}`);
|
|
857
893
|
return null;
|
|
@@ -887,8 +923,8 @@ class SecureStorage {
|
|
|
887
923
|
this.logger.timeEnd(`getItem:${key}`);
|
|
888
924
|
return null;
|
|
889
925
|
}
|
|
890
|
-
// Verify integrity if checksum exists
|
|
891
|
-
if (storedValue.checksum) {
|
|
926
|
+
// Verify integrity if checksum exists and configured
|
|
927
|
+
if (storedValue.checksum && this.config.storage.verifyIntegrityOnRead) {
|
|
892
928
|
const calculatedChecksum = await this.calculateChecksum(storedValue.value);
|
|
893
929
|
if (calculatedChecksum !== storedValue.checksum) {
|
|
894
930
|
this.logger.warn(`Integrity check failed for key: ${key}`);
|
|
@@ -906,6 +942,9 @@ class SecureStorage {
|
|
|
906
942
|
finalValue = await decompress(new Uint8Array(compressedData));
|
|
907
943
|
this.eventEmitter.emit('decompressed', { key });
|
|
908
944
|
}
|
|
945
|
+
if (this.config.storage.enableCache) {
|
|
946
|
+
this.memoryCache.set(key, { value: finalValue, expiresAt: storedValue.expiresAt });
|
|
947
|
+
}
|
|
909
948
|
this.eventEmitter.emit('decrypted', { key });
|
|
910
949
|
this.logger.timeEnd(`getItem:${key}`);
|
|
911
950
|
return finalValue;
|
|
@@ -914,7 +953,7 @@ class SecureStorage {
|
|
|
914
953
|
this.logger.error(`Error al recuperar dato encriptado para ${key}:`, error);
|
|
915
954
|
this.eventEmitter.emit('error', { key, error: error });
|
|
916
955
|
// Fallback: try plain key
|
|
917
|
-
const fallback =
|
|
956
|
+
const fallback = this.config.storage.storageEngine.getItem(key);
|
|
918
957
|
this.logger.timeEnd(`getItem:${key}`);
|
|
919
958
|
return fallback;
|
|
920
959
|
}
|
|
@@ -924,19 +963,20 @@ class SecureStorage {
|
|
|
924
963
|
*/
|
|
925
964
|
async removeItem(key) {
|
|
926
965
|
if (!EncryptionHelper.isSupported()) {
|
|
927
|
-
|
|
966
|
+
this.config.storage.storageEngine.removeItem(key);
|
|
928
967
|
return;
|
|
929
968
|
}
|
|
930
969
|
try {
|
|
931
970
|
const encryptedKey = await this.encryptionHelper.encryptKey(key);
|
|
932
|
-
|
|
933
|
-
|
|
971
|
+
this.config.storage.storageEngine.removeItem(encryptedKey);
|
|
972
|
+
this.config.storage.storageEngine.removeItem(key); // Remove both versions
|
|
973
|
+
this.memoryCache.delete(key);
|
|
934
974
|
this.eventEmitter.emit('deleted', { key });
|
|
935
975
|
this.logger.info(`Removed key: ${key}`);
|
|
936
976
|
}
|
|
937
977
|
catch (error) {
|
|
938
978
|
this.logger.error(`Error al eliminar dato para ${key}:`, error);
|
|
939
|
-
|
|
979
|
+
this.config.storage.storageEngine.removeItem(key);
|
|
940
980
|
}
|
|
941
981
|
}
|
|
942
982
|
/**
|
|
@@ -951,6 +991,7 @@ class SecureStorage {
|
|
|
951
991
|
*/
|
|
952
992
|
clear() {
|
|
953
993
|
this.encryptionHelper.clearEncryptedData();
|
|
994
|
+
this.memoryCache.clear();
|
|
954
995
|
this.eventEmitter.emit('cleared', {});
|
|
955
996
|
this.logger.info('All encrypted data cleared');
|
|
956
997
|
}
|
|
@@ -961,21 +1002,28 @@ class SecureStorage {
|
|
|
961
1002
|
this.logger.info('Starting cleanup of expired items...');
|
|
962
1003
|
let cleanedCount = 0;
|
|
963
1004
|
const keysToCheck = [];
|
|
964
|
-
for (let i = 0; i <
|
|
965
|
-
const key =
|
|
1005
|
+
for (let i = 0; i < this.config.storage.storageEngine.length; i++) {
|
|
1006
|
+
const key = this.config.storage.storageEngine.key(i);
|
|
966
1007
|
if (key && key.startsWith('__enc_')) {
|
|
967
1008
|
keysToCheck.push(key);
|
|
968
1009
|
}
|
|
969
1010
|
}
|
|
970
1011
|
for (const encryptedKey of keysToCheck) {
|
|
971
1012
|
try {
|
|
972
|
-
const encryptedValue =
|
|
1013
|
+
const encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
|
|
973
1014
|
if (!encryptedValue)
|
|
974
1015
|
continue;
|
|
975
1016
|
const decrypted = await this.encryptionHelper.decrypt(encryptedValue);
|
|
976
1017
|
const storedValue = JSON.parse(decrypted);
|
|
977
1018
|
if (storedValue.expiresAt && storedValue.expiresAt < Date.now()) {
|
|
978
|
-
|
|
1019
|
+
this.config.storage.storageEngine.removeItem(encryptedKey);
|
|
1020
|
+
// Encontrar la clave original en el cache y eliminarla
|
|
1021
|
+
for (const [cacheKey] of Array.from(this.memoryCache.entries())) {
|
|
1022
|
+
this.encryptionHelper.encryptKey(cacheKey).then(enc => {
|
|
1023
|
+
if (enc === encryptedKey)
|
|
1024
|
+
this.memoryCache.delete(cacheKey);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
979
1027
|
cleanedCount++;
|
|
980
1028
|
this.eventEmitter.emit('expired', { key: encryptedKey });
|
|
981
1029
|
}
|
|
@@ -993,7 +1041,7 @@ class SecureStorage {
|
|
|
993
1041
|
async verifyIntegrity(key) {
|
|
994
1042
|
try {
|
|
995
1043
|
const encryptedKey = await this.encryptionHelper.encryptKey(key);
|
|
996
|
-
const encryptedValue =
|
|
1044
|
+
const encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
|
|
997
1045
|
if (!encryptedValue)
|
|
998
1046
|
return false;
|
|
999
1047
|
const decrypted = await this.encryptionHelper.decrypt(encryptedValue);
|
|
@@ -1016,7 +1064,7 @@ class SecureStorage {
|
|
|
1016
1064
|
async getIntegrityInfo(key) {
|
|
1017
1065
|
try {
|
|
1018
1066
|
const encryptedKey = await this.encryptionHelper.encryptKey(key);
|
|
1019
|
-
const encryptedValue =
|
|
1067
|
+
const encryptedValue = this.config.storage.storageEngine.getItem(encryptedKey);
|
|
1020
1068
|
if (!encryptedValue)
|
|
1021
1069
|
return null;
|
|
1022
1070
|
const decrypted = await this.encryptionHelper.decrypt(encryptedValue);
|
|
@@ -1099,7 +1147,7 @@ class SecureStorage {
|
|
|
1099
1147
|
this.logger.info(`Iniciando migración de ${keys.length} claves...`);
|
|
1100
1148
|
for (const key of keys) {
|
|
1101
1149
|
try {
|
|
1102
|
-
const value =
|
|
1150
|
+
const value = this.config.storage.storageEngine.getItem(key);
|
|
1103
1151
|
if (value === null)
|
|
1104
1152
|
continue;
|
|
1105
1153
|
// Try to decrypt to check if already encrypted
|
|
@@ -1112,7 +1160,7 @@ class SecureStorage {
|
|
|
1112
1160
|
// Not encrypted, proceed with migration
|
|
1113
1161
|
}
|
|
1114
1162
|
await this.setItem(key, value);
|
|
1115
|
-
|
|
1163
|
+
this.config.storage.storageEngine.removeItem(key);
|
|
1116
1164
|
this.logger.info(`✓ ${key} migrado exitosamente`);
|
|
1117
1165
|
}
|
|
1118
1166
|
catch (error) {
|
|
@@ -1126,8 +1174,8 @@ class SecureStorage {
|
|
|
1126
1174
|
*/
|
|
1127
1175
|
getDebugInfo() {
|
|
1128
1176
|
const allKeys = [];
|
|
1129
|
-
for (let i = 0; i <
|
|
1130
|
-
const key =
|
|
1177
|
+
for (let i = 0; i < this.config.storage.storageEngine.length; i++) {
|
|
1178
|
+
const key = this.config.storage.storageEngine.key(i);
|
|
1131
1179
|
if (key)
|
|
1132
1180
|
allKeys.push(key);
|
|
1133
1181
|
}
|
|
@@ -1166,6 +1214,194 @@ class SecureStorage {
|
|
|
1166
1214
|
}
|
|
1167
1215
|
SecureStorage.instance = null;
|
|
1168
1216
|
|
|
1217
|
+
/**
|
|
1218
|
+
* SecureCookie - API de alto nivel para almacenamiento en cookies cifradas
|
|
1219
|
+
* Soporta opciones de cookies incluyendo domininios/subdominios y compresión.
|
|
1220
|
+
*/
|
|
1221
|
+
class SecureCookie {
|
|
1222
|
+
constructor(config) {
|
|
1223
|
+
this.config = {
|
|
1224
|
+
encryption: { ...DEFAULT_CONFIG.encryption, ...config?.encryption },
|
|
1225
|
+
cookieOptions: { path: '/', ...config?.cookieOptions },
|
|
1226
|
+
compression: config?.compression ?? true,
|
|
1227
|
+
debug: { ...DEFAULT_CONFIG.debug, ...config?.debug }
|
|
1228
|
+
};
|
|
1229
|
+
this.logger = new Logger(this.config.debug);
|
|
1230
|
+
this.encryptionHelper = new EncryptionHelper(this.config.encryption);
|
|
1231
|
+
this.logger.info('SecureCookie initialized', this.config);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Obtiene la instancia singleton de SecureCookie
|
|
1235
|
+
*/
|
|
1236
|
+
static getInstance(config) {
|
|
1237
|
+
if (!SecureCookie.instance) {
|
|
1238
|
+
SecureCookie.instance = new SecureCookie(config);
|
|
1239
|
+
}
|
|
1240
|
+
return SecureCookie.instance;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Serializa las opciones de cookie en un string
|
|
1244
|
+
*/
|
|
1245
|
+
serializeOptions(options) {
|
|
1246
|
+
let cookieString = '';
|
|
1247
|
+
if (options.expires) {
|
|
1248
|
+
cookieString += `; expires=${options.expires.toUTCString()}`;
|
|
1249
|
+
}
|
|
1250
|
+
if (options.maxAge) {
|
|
1251
|
+
cookieString += `; max-age=${options.maxAge}`;
|
|
1252
|
+
}
|
|
1253
|
+
if (options.domain) {
|
|
1254
|
+
cookieString += `; domain=${options.domain}`;
|
|
1255
|
+
}
|
|
1256
|
+
if (options.path) {
|
|
1257
|
+
cookieString += `; path=${options.path}`;
|
|
1258
|
+
}
|
|
1259
|
+
if (options.secure) {
|
|
1260
|
+
cookieString += `; secure`;
|
|
1261
|
+
}
|
|
1262
|
+
if (options.sameSite) {
|
|
1263
|
+
cookieString += `; samesite=${options.sameSite}`;
|
|
1264
|
+
}
|
|
1265
|
+
return cookieString;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Guarda un valor encriptado en una cookie
|
|
1269
|
+
*/
|
|
1270
|
+
async set(name, value, options) {
|
|
1271
|
+
this.logger.time(`cookie:set:${name}`);
|
|
1272
|
+
if (!EncryptionHelper.isSupported()) {
|
|
1273
|
+
this.logger.warn('Web Crypto API no soportada, guardando cookie sin cifrar');
|
|
1274
|
+
const mergedOptions = { ...this.config.cookieOptions, ...options };
|
|
1275
|
+
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}${this.serializeOptions(mergedOptions)}`;
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
// Check compression (aggressively compress cookies to save space since max is ~4KB)
|
|
1280
|
+
const shouldCompressData = this.config.compression && shouldCompress(value, 200);
|
|
1281
|
+
let processedValue = value;
|
|
1282
|
+
let metadataPrefix = 'P_'; // P_ = Plain
|
|
1283
|
+
if (shouldCompressData) {
|
|
1284
|
+
this.logger.debug(`Compressing cookie value for: ${name}`);
|
|
1285
|
+
const compressedData = await compress(value);
|
|
1286
|
+
processedValue = this.encryptionHelper['arrayBufferToBase64'](compressedData.buffer);
|
|
1287
|
+
metadataPrefix = 'C_'; // C_ = Compressed
|
|
1288
|
+
}
|
|
1289
|
+
// Encrypt key name
|
|
1290
|
+
const encryptedKey = await this.encryptionHelper.encryptKey(name);
|
|
1291
|
+
// Serialize and Encrypt Value
|
|
1292
|
+
// Adding metadata prefix to avoid bloating value with massive StoredValue JSON headers
|
|
1293
|
+
const encryptedData = await this.encryptionHelper.encrypt(processedValue);
|
|
1294
|
+
const finalValue = metadataPrefix + encryptedData;
|
|
1295
|
+
// Prepare Cookie string
|
|
1296
|
+
const mergedOptions = { ...this.config.cookieOptions, ...options };
|
|
1297
|
+
const cookieOptionsStr = this.serializeOptions(mergedOptions);
|
|
1298
|
+
const cookieString = `${encodeURIComponent(encryptedKey)}=${encodeURIComponent(finalValue)}${cookieOptionsStr}`;
|
|
1299
|
+
// Verificación de tamaño de cookie
|
|
1300
|
+
if (cookieString.length > 4096) {
|
|
1301
|
+
this.logger.warn(`Cookie '${name}' es muy grande (${cookieString.length} bytes). Puede ser rechazada por el navegador.`);
|
|
1302
|
+
}
|
|
1303
|
+
document.cookie = cookieString;
|
|
1304
|
+
this.logger.verbose(`Stored cookie: ${name}`);
|
|
1305
|
+
this.logger.timeEnd(`cookie:set:${name}`);
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
this.logger.error(`Error al guardar cookie encriptada para ${name}:`, error);
|
|
1309
|
+
// Fallback sin cifrar como último recurso
|
|
1310
|
+
const mergedOptions = { ...this.config.cookieOptions, ...options };
|
|
1311
|
+
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}${this.serializeOptions(mergedOptions)}`;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Pide una cookie por nombre
|
|
1316
|
+
* Retorna el string o null si no existe
|
|
1317
|
+
*/
|
|
1318
|
+
getRawCookie(name) {
|
|
1319
|
+
const nameEQ = encodeURIComponent(name) + '=';
|
|
1320
|
+
const ca = document.cookie.split(';');
|
|
1321
|
+
for (let i = 0; i < ca.length; i++) {
|
|
1322
|
+
let c = ca[i];
|
|
1323
|
+
while (c.charAt(0) === ' ')
|
|
1324
|
+
c = c.substring(1, c.length);
|
|
1325
|
+
if (c.indexOf(nameEQ) === 0)
|
|
1326
|
+
return decodeURIComponent(c.substring(nameEQ.length, c.length));
|
|
1327
|
+
}
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Recupera y desencripta un valor de cookie
|
|
1332
|
+
*/
|
|
1333
|
+
async get(name) {
|
|
1334
|
+
this.logger.time(`cookie:get:${name}`);
|
|
1335
|
+
if (!EncryptionHelper.isSupported()) {
|
|
1336
|
+
return this.getRawCookie(name);
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
// Find encrypted key
|
|
1340
|
+
const encryptedKey = await this.encryptionHelper.encryptKey(name);
|
|
1341
|
+
let rawValue = this.getRawCookie(encryptedKey);
|
|
1342
|
+
if (!rawValue) {
|
|
1343
|
+
// Backward compatibility just in case fallback was used
|
|
1344
|
+
rawValue = this.getRawCookie(name);
|
|
1345
|
+
if (!rawValue) {
|
|
1346
|
+
this.logger.timeEnd(`cookie:get:${name}`);
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
// Check if it has our metadata prefixes
|
|
1351
|
+
if (!rawValue.startsWith('P_') && !rawValue.startsWith('C_')) {
|
|
1352
|
+
// Podría ser un fallback plano
|
|
1353
|
+
this.logger.timeEnd(`cookie:get:${name}`);
|
|
1354
|
+
return rawValue; // Asumimos que es plano
|
|
1355
|
+
}
|
|
1356
|
+
const isCompressed = rawValue.startsWith('C_');
|
|
1357
|
+
const encryptedData = rawValue.substring(2);
|
|
1358
|
+
// Decrypt
|
|
1359
|
+
const decryptedString = await this.encryptionHelper.decrypt(encryptedData);
|
|
1360
|
+
// Decompress if needed
|
|
1361
|
+
let finalValue = decryptedString;
|
|
1362
|
+
if (isCompressed) {
|
|
1363
|
+
const compressedBuffer = this.encryptionHelper['base64ToArrayBuffer'](decryptedString);
|
|
1364
|
+
finalValue = await decompress(new Uint8Array(compressedBuffer));
|
|
1365
|
+
}
|
|
1366
|
+
this.logger.timeEnd(`cookie:get:${name}`);
|
|
1367
|
+
return finalValue;
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
this.logger.error(`Error al recuperar cookie encriptada para ${name}:`, error);
|
|
1371
|
+
// Fallback
|
|
1372
|
+
return this.getRawCookie(name);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Elimina una cookie
|
|
1377
|
+
*/
|
|
1378
|
+
async remove(name, options) {
|
|
1379
|
+
const mergedOptions = { ...this.config.cookieOptions, ...options };
|
|
1380
|
+
// Expirar la cookie configurándole una fecha del pasado
|
|
1381
|
+
const expireOptions = {
|
|
1382
|
+
...mergedOptions,
|
|
1383
|
+
expires: new Date(0),
|
|
1384
|
+
maxAge: 0
|
|
1385
|
+
};
|
|
1386
|
+
if (!EncryptionHelper.isSupported()) {
|
|
1387
|
+
document.cookie = `${encodeURIComponent(name)}=${this.serializeOptions(expireOptions)}`;
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const encryptedKey = await this.encryptionHelper.encryptKey(name);
|
|
1391
|
+
// Remove encrypted cookie
|
|
1392
|
+
document.cookie = `${encodeURIComponent(encryptedKey)}=${this.serializeOptions(expireOptions)}`;
|
|
1393
|
+
// Also remove plain version just in case
|
|
1394
|
+
document.cookie = `${encodeURIComponent(name)}=${this.serializeOptions(expireOptions)}`;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Limpia la instancia actual (útil para testing o refresco)
|
|
1398
|
+
*/
|
|
1399
|
+
destroy() {
|
|
1400
|
+
SecureCookie.instance = null;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
SecureCookie.instance = null;
|
|
1404
|
+
|
|
1169
1405
|
const secureStorage$2 = SecureStorage.getInstance();
|
|
1170
1406
|
/**
|
|
1171
1407
|
* Función de debug para verificar el estado del sistema de encriptación
|
|
@@ -1218,6 +1454,202 @@ async function forceMigration(customKeys) {
|
|
|
1218
1454
|
await debugEncryptionState();
|
|
1219
1455
|
}
|
|
1220
1456
|
|
|
1457
|
+
/**
|
|
1458
|
+
* PlainStorage - Una interfaz consistente con SecureStorage pero sin cifrado.
|
|
1459
|
+
* Envuelve localStorage o sessionStorage con serialización JSON.
|
|
1460
|
+
*/
|
|
1461
|
+
class PlainStorage {
|
|
1462
|
+
constructor(storageEngine = typeof window !== 'undefined' ? window.localStorage : {}) {
|
|
1463
|
+
this.storage = storageEngine;
|
|
1464
|
+
this.logger = new Logger({ prefix: 'PlainStorage' });
|
|
1465
|
+
}
|
|
1466
|
+
async set(key, value) {
|
|
1467
|
+
try {
|
|
1468
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
1469
|
+
this.storage.setItem(key, serialized);
|
|
1470
|
+
}
|
|
1471
|
+
catch (error) {
|
|
1472
|
+
this.logger.error(`Error saving unencrypted item ${key}:`, error);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
async get(key) {
|
|
1476
|
+
try {
|
|
1477
|
+
const value = this.storage.getItem(key);
|
|
1478
|
+
if (value === null)
|
|
1479
|
+
return null;
|
|
1480
|
+
try {
|
|
1481
|
+
return JSON.parse(value);
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
return value; // Fallback for plain strings
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
catch (error) {
|
|
1488
|
+
this.logger.error(`Error reading unencrypted item ${key}:`, error);
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
async remove(key) {
|
|
1493
|
+
this.storage.removeItem(key);
|
|
1494
|
+
}
|
|
1495
|
+
clear() {
|
|
1496
|
+
this.storage.clear();
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Crea una instancia de almacenamiento encriptado.
|
|
1502
|
+
* @param engine 'localStorage' o 'sessionStorage'
|
|
1503
|
+
* @param secretKey Llave secreta opcional para derivación de claves
|
|
1504
|
+
* @param config Configuración adicional opcional
|
|
1505
|
+
* @returns Instancia de SecureStorage
|
|
1506
|
+
*/
|
|
1507
|
+
function createEncryptedStorage(engine = 'localStorage', secretKey, config) {
|
|
1508
|
+
const defaultEngine = typeof window !== 'undefined'
|
|
1509
|
+
? (engine === 'sessionStorage' ? window.sessionStorage : window.localStorage)
|
|
1510
|
+
: {};
|
|
1511
|
+
const mergedConfig = {
|
|
1512
|
+
...config,
|
|
1513
|
+
encryption: {
|
|
1514
|
+
...config?.encryption,
|
|
1515
|
+
...(secretKey ? { appIdentifier: secretKey } : {})
|
|
1516
|
+
},
|
|
1517
|
+
storage: {
|
|
1518
|
+
...config?.storage,
|
|
1519
|
+
storageEngine: defaultEngine
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
return SecureStorage.getInstance(mergedConfig);
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Crea una instancia de almacenamiento plano (sin cifrar).
|
|
1526
|
+
* @param engine 'localStorage' o 'sessionStorage'
|
|
1527
|
+
* @returns Instancia de PlainStorage
|
|
1528
|
+
*/
|
|
1529
|
+
function createPlainStorage(engine = 'localStorage') {
|
|
1530
|
+
const defaultEngine = typeof window !== 'undefined'
|
|
1531
|
+
? (engine === 'sessionStorage' ? window.sessionStorage : window.localStorage)
|
|
1532
|
+
: {};
|
|
1533
|
+
return new PlainStorage(defaultEngine);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* PlainCookie - Una interfaz consistente con SecureCookie pero sin cifrado.
|
|
1538
|
+
*/
|
|
1539
|
+
class PlainCookie {
|
|
1540
|
+
constructor(defaultOptions) {
|
|
1541
|
+
this.logger = new Logger({ prefix: 'PlainCookie' });
|
|
1542
|
+
this.defaultOptions = { path: '/', ...defaultOptions };
|
|
1543
|
+
}
|
|
1544
|
+
serializeOptions(options) {
|
|
1545
|
+
let cookieString = '';
|
|
1546
|
+
if (options.expires)
|
|
1547
|
+
cookieString += `; expires=${options.expires.toUTCString()}`;
|
|
1548
|
+
if (options.maxAge)
|
|
1549
|
+
cookieString += `; max-age=${options.maxAge}`;
|
|
1550
|
+
if (options.domain)
|
|
1551
|
+
cookieString += `; domain=${options.domain}`;
|
|
1552
|
+
if (options.path)
|
|
1553
|
+
cookieString += `; path=${options.path}`;
|
|
1554
|
+
if (options.secure)
|
|
1555
|
+
cookieString += `; secure`;
|
|
1556
|
+
if (options.sameSite)
|
|
1557
|
+
cookieString += `; samesite=${options.sameSite}`;
|
|
1558
|
+
return cookieString;
|
|
1559
|
+
}
|
|
1560
|
+
async set(name, value, options) {
|
|
1561
|
+
try {
|
|
1562
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
1563
|
+
const mergedOptions = { ...this.defaultOptions, ...options };
|
|
1564
|
+
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(serialized)}${this.serializeOptions(mergedOptions)}`;
|
|
1565
|
+
}
|
|
1566
|
+
catch (error) {
|
|
1567
|
+
this.logger.error(`Error saving unencrypted cookie ${name}:`, error);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
async get(name) {
|
|
1571
|
+
try {
|
|
1572
|
+
const nameEQ = encodeURIComponent(name) + '=';
|
|
1573
|
+
const ca = document.cookie.split(';');
|
|
1574
|
+
for (let i = 0; i < ca.length; i++) {
|
|
1575
|
+
let c = ca[i];
|
|
1576
|
+
while (c.charAt(0) === ' ')
|
|
1577
|
+
c = c.substring(1, c.length);
|
|
1578
|
+
if (c.indexOf(nameEQ) === 0) {
|
|
1579
|
+
const decoded = decodeURIComponent(c.substring(nameEQ.length, c.length));
|
|
1580
|
+
try {
|
|
1581
|
+
return JSON.parse(decoded);
|
|
1582
|
+
}
|
|
1583
|
+
catch {
|
|
1584
|
+
return decoded;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return null;
|
|
1589
|
+
}
|
|
1590
|
+
catch (error) {
|
|
1591
|
+
this.logger.error(`Error reading unencrypted cookie ${name}:`, error);
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
async remove(name, options) {
|
|
1596
|
+
const mergedOptions = { ...this.defaultOptions, ...options };
|
|
1597
|
+
document.cookie = `${encodeURIComponent(name)}=${this.serializeOptions({ ...mergedOptions, expires: new Date(0), maxAge: 0 })}`;
|
|
1598
|
+
}
|
|
1599
|
+
clearAll() {
|
|
1600
|
+
const cookies = document.cookie.split(';');
|
|
1601
|
+
for (let i = 0; i < cookies.length; i++) {
|
|
1602
|
+
const cookie = cookies[i];
|
|
1603
|
+
const eqPos = cookie.indexOf('=');
|
|
1604
|
+
const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim();
|
|
1605
|
+
this.remove(decodeURIComponent(name));
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Crea una instancia de manejador de cookies encriptadas.
|
|
1612
|
+
* @param secretKey Llave secreta opcional para derivación de claves
|
|
1613
|
+
* @param domain Dominio opcional (incluir punto inicial para subdominios ej. '.dominio.com')
|
|
1614
|
+
* @param config Configuración adicional opcional
|
|
1615
|
+
* @returns Instancia de SecureCookie
|
|
1616
|
+
*/
|
|
1617
|
+
function createEncryptedCookieManager(secretKey, domain, config) {
|
|
1618
|
+
const mergedConfig = {
|
|
1619
|
+
...config,
|
|
1620
|
+
encryption: {
|
|
1621
|
+
...config?.encryption,
|
|
1622
|
+
...(secretKey ? { appIdentifier: secretKey } : {})
|
|
1623
|
+
},
|
|
1624
|
+
cookieOptions: {
|
|
1625
|
+
...config?.cookieOptions,
|
|
1626
|
+
...(domain ? { domain } : {})
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
return SecureCookie.getInstance(mergedConfig);
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Crea una instancia de manejador de cookies plano (sin cifrar).
|
|
1633
|
+
* @param defaultOptions Opciones por defecto para las cookies (ej. domain)
|
|
1634
|
+
* @returns Instancia de PlainCookie
|
|
1635
|
+
*/
|
|
1636
|
+
function createPlainCookieManager(defaultOptions) {
|
|
1637
|
+
return new PlainCookie(defaultOptions);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Instancia predeterminada de almacenamiento local (sin cifrar).
|
|
1642
|
+
*/
|
|
1643
|
+
const localStore = createPlainStorage('localStorage');
|
|
1644
|
+
/**
|
|
1645
|
+
* Instancia predeterminada de almacenamiento de sesión (sin cifrar).
|
|
1646
|
+
*/
|
|
1647
|
+
const sessionStore = createPlainStorage('sessionStorage');
|
|
1648
|
+
/**
|
|
1649
|
+
* Instancia predeterminada de gestor de cookies (sin cifrar).
|
|
1650
|
+
*/
|
|
1651
|
+
const cookies = createPlainCookieManager();
|
|
1652
|
+
|
|
1221
1653
|
/**
|
|
1222
1654
|
* @bantis/local-cipher - Core Module
|
|
1223
1655
|
* Framework-agnostic client-side encryption for browser storage
|
|
@@ -1227,8 +1659,9 @@ async function forceMigration(customKeys) {
|
|
|
1227
1659
|
*/
|
|
1228
1660
|
// Core classes
|
|
1229
1661
|
const secureStorage$1 = SecureStorage.getInstance();
|
|
1662
|
+
const secureCookie = SecureCookie.getInstance();
|
|
1230
1663
|
// Version
|
|
1231
|
-
const VERSION = '2.
|
|
1664
|
+
const VERSION = '2.2.0';
|
|
1232
1665
|
|
|
1233
1666
|
/**
|
|
1234
1667
|
* React Hooks for @bantis/local-cipher
|
|
@@ -1408,18 +1841,35 @@ function useSecureStorageDebug(storage) {
|
|
|
1408
1841
|
}, [secureStorage]);
|
|
1409
1842
|
return debugInfo;
|
|
1410
1843
|
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Hook para usar un namespace de SecureStorage
|
|
1413
|
-
*/
|
|
1414
|
-
function useNamespace(namespace, storage) {
|
|
1415
|
-
const secureStorage = storage || getDefaultStorage();
|
|
1416
|
-
const [namespacedStorage] = react.useState(() => secureStorage.namespace(namespace));
|
|
1417
|
-
return namespacedStorage;
|
|
1418
|
-
}
|
|
1419
1844
|
/**
|
|
1420
1845
|
* Exportar la instancia de SecureStorage para uso directo
|
|
1421
1846
|
*/
|
|
1422
1847
|
const secureStorage = getDefaultStorage();
|
|
1848
|
+
/**
|
|
1849
|
+
* Hook para sincronizar el estado de React con el Storage Manager (por defecto localStore en texto plano)
|
|
1850
|
+
*/
|
|
1851
|
+
function useLocalStore(key, initialValue, storageManager = localStore) {
|
|
1852
|
+
const [value, setValue] = react.useState(initialValue);
|
|
1853
|
+
react.useEffect(() => {
|
|
1854
|
+
let mounted = true;
|
|
1855
|
+
storageManager.get(key).then((saved) => {
|
|
1856
|
+
if (!mounted)
|
|
1857
|
+
return;
|
|
1858
|
+
if (saved !== null) {
|
|
1859
|
+
setValue(saved);
|
|
1860
|
+
}
|
|
1861
|
+
else {
|
|
1862
|
+
storageManager.set(key, initialValue);
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
return () => { mounted = false; };
|
|
1866
|
+
}, [key, storageManager]);
|
|
1867
|
+
const setNewValue = react.useCallback(async (newValue) => {
|
|
1868
|
+
setValue(newValue);
|
|
1869
|
+
await storageManager.set(key, newValue);
|
|
1870
|
+
}, [key, storageManager]);
|
|
1871
|
+
return [value, setNewValue];
|
|
1872
|
+
}
|
|
1423
1873
|
|
|
1424
1874
|
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
1425
1875
|
exports.EncryptionHelper = EncryptionHelper;
|
|
@@ -1428,19 +1878,30 @@ exports.KeyRotation = KeyRotation;
|
|
|
1428
1878
|
exports.LIBRARY_VERSION = LIBRARY_VERSION;
|
|
1429
1879
|
exports.Logger = Logger;
|
|
1430
1880
|
exports.NamespacedStorage = NamespacedStorage;
|
|
1881
|
+
exports.PlainCookie = PlainCookie;
|
|
1882
|
+
exports.PlainStorage = PlainStorage;
|
|
1431
1883
|
exports.STORAGE_VERSION = STORAGE_VERSION;
|
|
1884
|
+
exports.SecureCookie = SecureCookie;
|
|
1432
1885
|
exports.SecureStorage = SecureStorage;
|
|
1433
1886
|
exports.VERSION = VERSION;
|
|
1434
1887
|
exports.compress = compress;
|
|
1888
|
+
exports.cookies = cookies;
|
|
1889
|
+
exports.createEncryptedCookieManager = createEncryptedCookieManager;
|
|
1890
|
+
exports.createEncryptedStorage = createEncryptedStorage;
|
|
1891
|
+
exports.createPlainCookieManager = createPlainCookieManager;
|
|
1892
|
+
exports.createPlainStorage = createPlainStorage;
|
|
1435
1893
|
exports.debugEncryptionState = debugEncryptionState;
|
|
1436
1894
|
exports.decompress = decompress;
|
|
1437
1895
|
exports.forceMigration = forceMigration;
|
|
1438
1896
|
exports.initializeSecureStorage = initializeSecureStorage;
|
|
1439
1897
|
exports.isCompressionSupported = isCompressionSupported;
|
|
1898
|
+
exports.localStore = localStore;
|
|
1440
1899
|
exports.reactSecureStorage = secureStorage;
|
|
1900
|
+
exports.secureCookie = secureCookie;
|
|
1441
1901
|
exports.secureStorage = secureStorage$1;
|
|
1902
|
+
exports.sessionStore = sessionStore;
|
|
1442
1903
|
exports.shouldCompress = shouldCompress;
|
|
1443
|
-
exports.
|
|
1904
|
+
exports.useLocalStore = useLocalStore;
|
|
1444
1905
|
exports.useSecureStorage = useSecureStorage;
|
|
1445
1906
|
exports.useSecureStorageDebug = useSecureStorageDebug;
|
|
1446
1907
|
exports.useSecureStorageEvents = useSecureStorageEvents;
|