@nextage/nx-frame-be 1.0.43 → 1.0.44

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("../utils");
4
+ /**
5
+ * `randomString` must return a URL-safe token of the requested length. The
6
+ * base64url alphabet (`A-Za-z0-9-_`) avoids the `+`/`/`/`=` characters that the
7
+ * previous base64 implementation could emit, so the value is safe in URLs,
8
+ * tokens and filenames.
9
+ */
10
+ describe('randomString', () => {
11
+ const URL_SAFE = /^[A-Za-z0-9_-]+$/;
12
+ it('returns a string of the requested length (default 21)', () => {
13
+ expect((0, utils_1.randomString)()).toHaveLength(21);
14
+ expect((0, utils_1.randomString)(7)).toHaveLength(7);
15
+ expect((0, utils_1.randomString)(32)).toHaveLength(32);
16
+ });
17
+ it('only uses URL-safe characters (no +, /, =)', () => {
18
+ for (const size of [7, 12, 21, 40]) {
19
+ const value = (0, utils_1.randomString)(size);
20
+ expect(value).toMatch(URL_SAFE);
21
+ expect(value).not.toContain('+');
22
+ expect(value).not.toContain('/');
23
+ expect(value).not.toContain('=');
24
+ }
25
+ });
26
+ it('is effectively unique across calls', () => {
27
+ const values = new Set(Array.from({ length: 500 }, () => (0, utils_1.randomString)(21)));
28
+ expect(values.size).toBe(500);
29
+ });
30
+ });
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const constants_1 = require("../../constants");
4
+ const crypto_legacy_1 = require("../crypto-legacy");
5
+ const crypto_constants_1 = require("../crypto.constants");
6
+ /**
7
+ * `CryptoUtils` migrated off `crypto-js` onto `node:crypto`.
8
+ *
9
+ * These tests pin two contracts:
10
+ * 1. the hash/PBKDF2 helpers stay BYTE-COMPATIBLE with the previous CryptoJS
11
+ * output (oracle vectors captured while crypto-js was still installed), so
12
+ * existing blind indexes keep matching;
13
+ * 2. field encryption uses authenticated AES-256-GCM (`v1:` scheme) while still
14
+ * decrypting the legacy OpenSSL `Salted__` format for backward compatibility.
15
+ */
16
+ describe('CryptoUtils (node:crypto)', () => {
17
+ // Oracle vectors produced by crypto-js 4.2.0 before its removal.
18
+ const ORACLE = {
19
+ md5: '5d41402abc4b2a76b9719d911017c592',
20
+ sha256: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
21
+ sha512: '9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043',
22
+ pbkdf2: '64747239499f1befef7f59618f443dd8f2ec330698a919978a6b4d90b14aa915',
23
+ // AES.encrypt('secret-data-àéì', 'passphrase-123')
24
+ legacy: 'U2FsdGVkX18FAy7zi7VFY7AD1ebKT3xBwKo9UavXDMgWA1DKBQ4RHgnYaWlV7gof',
25
+ };
26
+ describe('hashes stay byte-compatible with crypto-js', () => {
27
+ it('md5', () => expect(constants_1.cryptoUtils.md5('hello')).toBe(ORACLE.md5));
28
+ it('sha256', () => expect(constants_1.cryptoUtils.sha256('hello')).toBe(ORACLE.sha256));
29
+ it('sha512', () => expect(constants_1.cryptoUtils.sha512('hello')).toBe(ORACLE.sha512));
30
+ it('pbkdf2 blind index (256-bit, sha256, 10000)', () => {
31
+ expect(constants_1.cryptoUtils.pbkdf2('value', 'salt')).toBe(ORACLE.pbkdf2);
32
+ });
33
+ it('empty input returns empty string', () => {
34
+ expect(constants_1.cryptoUtils.md5('')).toBe('');
35
+ expect(constants_1.cryptoUtils.sha256('')).toBe('');
36
+ expect(constants_1.cryptoUtils.sha512('')).toBe('');
37
+ expect(constants_1.cryptoUtils.pbkdf2('', 'salt')).toBe('');
38
+ });
39
+ });
40
+ describe('field encryption (AES-256-GCM v1)', () => {
41
+ const secret = 'test-secret-passphrase';
42
+ it('round-trips a value and stamps the v1 prefix', () => {
43
+ const plain = 'user@example.com';
44
+ const enc = constants_1.cryptoUtils.encryptText(plain, secret);
45
+ expect(enc.startsWith(crypto_constants_1.FIELD_ENC_PREFIX)).toBe(true);
46
+ expect(enc).not.toContain(plain);
47
+ expect(constants_1.cryptoUtils.decryptText(enc, secret)).toBe(plain);
48
+ });
49
+ it('produces a different ciphertext each time (random IV)', () => {
50
+ const a = constants_1.cryptoUtils.encryptText('same', secret);
51
+ const b = constants_1.cryptoUtils.encryptText('same', secret);
52
+ expect(a).not.toBe(b);
53
+ expect(constants_1.cryptoUtils.decryptText(a, secret)).toBe('same');
54
+ expect(constants_1.cryptoUtils.decryptText(b, secret)).toBe('same');
55
+ });
56
+ it('handles unicode', () => {
57
+ const plain = 'àéìòù — 日本語 — €';
58
+ const enc = constants_1.cryptoUtils.encryptText(plain, secret);
59
+ expect(constants_1.cryptoUtils.decryptText(enc, secret)).toBe(plain);
60
+ });
61
+ it('returns empty string on empty input', () => {
62
+ expect(constants_1.cryptoUtils.encryptText('', secret)).toBe('');
63
+ expect(constants_1.cryptoUtils.decryptText('', secret)).toBe('');
64
+ expect(constants_1.cryptoUtils.encryptText('x', '')).toBe('');
65
+ });
66
+ it('fails closed on a tampered ciphertext (GCM auth)', () => {
67
+ const enc = constants_1.cryptoUtils.encryptText('sensitive', secret);
68
+ // Flip a character in the base64 body → auth tag no longer verifies.
69
+ const body = enc.slice(crypto_constants_1.FIELD_ENC_PREFIX.length);
70
+ const tampered = crypto_constants_1.FIELD_ENC_PREFIX + (body[0] === 'A' ? 'B' : 'A') + body.slice(1);
71
+ expect(constants_1.cryptoUtils.decryptText(tampered, secret)).toBe('');
72
+ });
73
+ it('wrong secret does not decrypt', () => {
74
+ const enc = constants_1.cryptoUtils.encryptText('sensitive', secret);
75
+ expect(constants_1.cryptoUtils.decryptText(enc, 'other-secret')).toBe('');
76
+ });
77
+ it('re-rotation: decrypt with old secret, re-encrypt with new secret', () => {
78
+ const plain = 'rotate-me';
79
+ const v1old = constants_1.cryptoUtils.encryptText(plain, 'old-secret');
80
+ // Simulate the migration engine's rotation step.
81
+ const roundTrip = constants_1.cryptoUtils.decryptText(v1old, 'old-secret');
82
+ const v1new = constants_1.cryptoUtils.encryptText(roundTrip, 'new-secret');
83
+ expect(constants_1.cryptoUtils.decryptText(v1new, 'new-secret')).toBe(plain);
84
+ expect(constants_1.cryptoUtils.decryptText(v1new, 'old-secret')).toBe('');
85
+ });
86
+ });
87
+ describe('legacy CryptoJS compatibility', () => {
88
+ it('detects a legacy Salted__ ciphertext', () => {
89
+ expect((0, crypto_legacy_1.isLegacyCiphertext)(ORACLE.legacy)).toBe(true);
90
+ expect((0, crypto_legacy_1.isLegacyCiphertext)('v1:abc')).toBe(false);
91
+ expect((0, crypto_legacy_1.isLegacyCiphertext)('plain text')).toBe(false);
92
+ expect((0, crypto_legacy_1.isLegacyCiphertext)('')).toBe(false);
93
+ });
94
+ it('decrypts a real crypto-js ciphertext via node:crypto', () => {
95
+ expect((0, crypto_legacy_1.decryptLegacyAes)(ORACLE.legacy, 'passphrase-123')).toBe('secret-data-àéì');
96
+ });
97
+ it('decryptText transparently reads legacy values', () => {
98
+ expect(constants_1.cryptoUtils.decryptText(ORACLE.legacy, 'passphrase-123')).toBe('secret-data-àéì');
99
+ });
100
+ });
101
+ });
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const user_model_1 = require("../../models/user.model");
13
+ const constants_1 = require("../../constants");
14
+ const migrate_encrypted_fields_1 = require("../migrate-encrypted-fields");
15
+ const crypto_constants_1 = require("../crypto.constants");
16
+ /**
17
+ * `reEncryptLegacyFields` bulk-migrates legacy CryptoJS field ciphertexts to the
18
+ * current AES-256-GCM scheme, operating at the native driver level so it neither
19
+ * re-runs getters (read) nor setters (write). These tests seed a raw legacy value
20
+ * (produced by crypto-js 4.2.0 with secret 'aaa'), migrate it, and assert the
21
+ * value becomes readable `v1:` — and that a second run is a no-op (idempotent).
22
+ */
23
+ describe('reEncryptLegacyFields', () => {
24
+ const SECRET = 'aaa'; // matches src/test/env.ts
25
+ // crypto-js AES.encrypt('mario.rossi@example.com', 'aaa') captured pre-removal.
26
+ const LEGACY_EMAIL = 'U2FsdGVkX18wA04OcAdQSqF5R07te9XD+nuXAMAbCaI8kSXFdK7bNBdSjKPyIvrX';
27
+ const LEGACY_PLAINTEXT = 'mario.rossi@example.com';
28
+ it('migrates a legacy value to the v1 GCM scheme and stays idempotent', () => __awaiter(void 0, void 0, void 0, function* () {
29
+ const collection = user_model_1.users.mgModel.collection;
30
+ // Seed a raw document with a legacy-encrypted email (bypassing setters).
31
+ const { insertedId } = yield collection.insertOne({
32
+ username: 'mario',
33
+ email: LEGACY_EMAIL,
34
+ version: 0,
35
+ });
36
+ // --- first run: should migrate the field ---
37
+ const res1 = yield (0, migrate_encrypted_fields_1.reEncryptLegacyFields)({
38
+ targets: [{ model: user_model_1.users, fields: ['email'] }],
39
+ secret: SECRET,
40
+ });
41
+ expect(res1.migrated).toBe(1);
42
+ expect(res1.errors).toBe(0);
43
+ const migrated = yield collection.findOne({ _id: insertedId });
44
+ expect(migrated.email.startsWith(crypto_constants_1.FIELD_ENC_PREFIX)).toBe(true);
45
+ expect(constants_1.cryptoUtils.decryptText(migrated.email, SECRET)).toBe(LEGACY_PLAINTEXT);
46
+ // --- second run: nothing left to migrate ---
47
+ const res2 = yield (0, migrate_encrypted_fields_1.reEncryptLegacyFields)({
48
+ targets: [{ model: user_model_1.users, fields: ['email'] }],
49
+ secret: SECRET,
50
+ });
51
+ expect(res2.migrated).toBe(0);
52
+ expect(res2.skipped).toBe(1);
53
+ }));
54
+ it('dry-run reports the work without writing', () => __awaiter(void 0, void 0, void 0, function* () {
55
+ const collection = user_model_1.users.mgModel.collection;
56
+ const { insertedId } = yield collection.insertOne({
57
+ username: 'anna',
58
+ email: LEGACY_EMAIL,
59
+ version: 0,
60
+ });
61
+ const res = yield (0, migrate_encrypted_fields_1.reEncryptLegacyFields)({
62
+ targets: [{ model: user_model_1.users, fields: ['email'] }],
63
+ secret: SECRET,
64
+ dryRun: true,
65
+ });
66
+ expect(res.migrated).toBe(1);
67
+ const untouched = yield collection.findOne({ _id: insertedId });
68
+ expect(untouched.email).toBe(LEGACY_EMAIL); // unchanged on disk
69
+ }));
70
+ it('skips empty and already-migrated values', () => __awaiter(void 0, void 0, void 0, function* () {
71
+ const collection = user_model_1.users.mgModel.collection;
72
+ yield collection.insertOne({ username: 'empty', email: '', version: 0 });
73
+ yield collection.insertOne({
74
+ username: 'fresh',
75
+ email: constants_1.cryptoUtils.encryptText('fresh@example.com', SECRET),
76
+ version: 0,
77
+ });
78
+ const res = yield (0, migrate_encrypted_fields_1.reEncryptLegacyFields)({
79
+ targets: [{ model: user_model_1.users, fields: ['email'] }],
80
+ secret: SECRET,
81
+ });
82
+ expect(res.migrated).toBe(0);
83
+ expect(res.skipped).toBe(2);
84
+ }));
85
+ it('migrates a nested field without wiping its siblings', () => __awaiter(void 0, void 0, void 0, function* () {
86
+ const collection = user_model_1.users.mgModel.collection;
87
+ // Only `profile.email` is legacy; firstname/lastname are plain siblings that
88
+ // MUST survive (a nested-object $set would replace the whole `profile`).
89
+ const { insertedId } = yield collection.insertOne({
90
+ username: 'nested',
91
+ profile: { email: LEGACY_EMAIL, firstname: 'Mario', lastname: 'Rossi' },
92
+ version: 0,
93
+ });
94
+ const res = yield (0, migrate_encrypted_fields_1.reEncryptLegacyFields)({
95
+ targets: [{ model: user_model_1.users, fields: ['profile.email'] }],
96
+ secret: SECRET,
97
+ });
98
+ expect(res.migrated).toBe(1);
99
+ const doc = yield collection.findOne({ _id: insertedId });
100
+ expect(doc.profile.firstname).toBe('Mario'); // sibling preserved
101
+ expect(doc.profile.lastname).toBe('Rossi'); // sibling preserved
102
+ expect(doc.profile.email.startsWith(crypto_constants_1.FIELD_ENC_PREFIX)).toBe(true);
103
+ expect(constants_1.cryptoUtils.decryptText(doc.profile.email, SECRET)).toBe(LEGACY_PLAINTEXT);
104
+ }));
105
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Detects whether a stored value is a legacy CryptoJS/OpenSSL ciphertext.
3
+ *
4
+ * @param text the stored field value
5
+ * @returns `true` if it base64-decodes to the `Salted__` header
6
+ */
7
+ export declare function isLegacyCiphertext(text: unknown): boolean;
8
+ /**
9
+ * Decrypts a legacy CryptoJS/OpenSSL `Salted__` ciphertext.
10
+ *
11
+ * @param text the base64 legacy ciphertext
12
+ * @param secret the passphrase used to encrypt it
13
+ * @returns the decrypted UTF-8 plaintext
14
+ * @throws if the payload is not a valid `Salted__` block
15
+ */
16
+ export declare function decryptLegacyAes(text: string, secret: string): string;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isLegacyCiphertext = isLegacyCiphertext;
7
+ exports.decryptLegacyAes = decryptLegacyAes;
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ const crypto_constants_1 = require("./crypto.constants");
10
+ /**
11
+ * Backward-compatibility decryptor for values produced by the previous
12
+ * `CryptoJS.AES.encrypt(text, passphrase)` implementation.
13
+ *
14
+ * CryptoJS emits the OpenSSL format: base64 of `"Salted__" | salt(8) |
15
+ * ciphertext`, where the AES-256-CBC key and IV are derived from the passphrase
16
+ * and salt with OpenSSL's `EVP_BytesToKey` (one round of MD5). This module
17
+ * re-implements that derivation with `node:crypto`, so `crypto-js` can be
18
+ * dropped while existing ciphertexts remain readable.
19
+ */
20
+ const SALT_HEADER_BYTES = 8;
21
+ const SALT_BYTES = 8;
22
+ const AES_KEY_BYTES = 32; // AES-256
23
+ const AES_IV_BYTES = 16; // CBC IV
24
+ /**
25
+ * OpenSSL `EVP_BytesToKey` with a single MD5 digest per block.
26
+ *
27
+ * Derives `keyLen + ivLen` bytes by hashing `previousBlock | password | salt`
28
+ * repeatedly, exactly as OpenSSL/CryptoJS do for `AES.encrypt(text, passphrase)`.
29
+ *
30
+ * @param password the passphrase (secret) as used by CryptoJS
31
+ * @param salt the 8-byte salt extracted from the payload
32
+ * @param keyLen number of key bytes to derive (32 for AES-256)
33
+ * @param ivLen number of IV bytes to derive (16 for CBC)
34
+ */
35
+ function evpBytesToKey(password, salt, keyLen, ivLen) {
36
+ const pwd = Buffer.from(password, 'utf8');
37
+ let derived = Buffer.alloc(0);
38
+ let block = Buffer.alloc(0);
39
+ while (derived.length < keyLen + ivLen) {
40
+ block = node_crypto_1.default.createHash('md5').update(Buffer.concat([block, pwd, salt])).digest();
41
+ derived = Buffer.concat([derived, block]);
42
+ }
43
+ return {
44
+ key: derived.subarray(0, keyLen),
45
+ iv: derived.subarray(keyLen, keyLen + ivLen),
46
+ };
47
+ }
48
+ /**
49
+ * Detects whether a stored value is a legacy CryptoJS/OpenSSL ciphertext.
50
+ *
51
+ * @param text the stored field value
52
+ * @returns `true` if it base64-decodes to the `Salted__` header
53
+ */
54
+ function isLegacyCiphertext(text) {
55
+ if (!text || typeof text !== 'string')
56
+ return false;
57
+ try {
58
+ const raw = Buffer.from(text, 'base64');
59
+ return raw.length > SALT_HEADER_BYTES + SALT_BYTES
60
+ && raw.subarray(0, SALT_HEADER_BYTES).toString('latin1') === crypto_constants_1.LEGACY_ENC_MAGIC;
61
+ }
62
+ catch (_a) {
63
+ return false;
64
+ }
65
+ }
66
+ /**
67
+ * Decrypts a legacy CryptoJS/OpenSSL `Salted__` ciphertext.
68
+ *
69
+ * @param text the base64 legacy ciphertext
70
+ * @param secret the passphrase used to encrypt it
71
+ * @returns the decrypted UTF-8 plaintext
72
+ * @throws if the payload is not a valid `Salted__` block
73
+ */
74
+ function decryptLegacyAes(text, secret) {
75
+ const raw = Buffer.from(text, 'base64');
76
+ if (raw.subarray(0, SALT_HEADER_BYTES).toString('latin1') !== crypto_constants_1.LEGACY_ENC_MAGIC)
77
+ throw new Error('Not a legacy Salted__ ciphertext');
78
+ const salt = raw.subarray(SALT_HEADER_BYTES, SALT_HEADER_BYTES + SALT_BYTES);
79
+ const ciphertext = raw.subarray(SALT_HEADER_BYTES + SALT_BYTES);
80
+ const { key, iv } = evpBytesToKey(secret, salt, AES_KEY_BYTES, AES_IV_BYTES);
81
+ const decipher = node_crypto_1.default.createDecipheriv('aes-256-cbc', key, iv);
82
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
83
+ }
@@ -5,6 +5,17 @@ export declare const CRYPTO_SYM_BITSIZE = 256;
5
5
  export declare const CRYPTO_ASYM_BITSIZE_LIST: number[];
6
6
  export declare const CRYPTO_PUB_KEY_ENCODING: KeyEncoding;
7
7
  export declare const CRYPTO_PRIV_KEY_ENCODING: KeyEncoding;
8
+ export declare const FIELD_ENC_VERSION = "v1";
9
+ export declare const FIELD_ENC_PREFIX = "v1:";
10
+ export declare const FIELD_ENC_ALGO = "aes-256-gcm";
11
+ export declare const FIELD_ENC_IV_BYTES = 12;
12
+ export declare const FIELD_ENC_TAG_BYTES = 16;
13
+ export declare const FIELD_ENC_KEY_BYTES = 32;
14
+ export declare const FIELD_ENC_KDF_SALT = "nxfb.field.v1";
15
+ export declare const FIELD_ENC_KDF_N = 16384;
16
+ export declare const FIELD_ENC_KDF_R = 8;
17
+ export declare const FIELD_ENC_KDF_P = 1;
18
+ export declare const LEGACY_ENC_MAGIC = "Salted__";
8
19
  export declare const CRYPTO_SYM_BITSIZE_MAP: NxTypeObject<KeySizeMap>;
9
20
  export declare enum CRYPTO_ERR_CODES {
10
21
  BITSIZE = "ERR_INVALID_BITSIZE",
@@ -1,11 +1,38 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CRYPTO_ERRORS = exports.CRYPTO_ERR_CODES = exports.CRYPTO_SYM_BITSIZE_MAP = exports.CRYPTO_PRIV_KEY_ENCODING = exports.CRYPTO_PUB_KEY_ENCODING = exports.CRYPTO_ASYM_BITSIZE_LIST = exports.CRYPTO_SYM_BITSIZE = exports.CRYPTO_ASYM_BITSIZE = void 0;
3
+ exports.CRYPTO_ERRORS = exports.CRYPTO_ERR_CODES = exports.CRYPTO_SYM_BITSIZE_MAP = exports.LEGACY_ENC_MAGIC = exports.FIELD_ENC_KDF_P = exports.FIELD_ENC_KDF_R = exports.FIELD_ENC_KDF_N = exports.FIELD_ENC_KDF_SALT = exports.FIELD_ENC_KEY_BYTES = exports.FIELD_ENC_TAG_BYTES = exports.FIELD_ENC_IV_BYTES = exports.FIELD_ENC_ALGO = exports.FIELD_ENC_PREFIX = exports.FIELD_ENC_VERSION = exports.CRYPTO_PRIV_KEY_ENCODING = exports.CRYPTO_PUB_KEY_ENCODING = exports.CRYPTO_ASYM_BITSIZE_LIST = exports.CRYPTO_SYM_BITSIZE = exports.CRYPTO_ASYM_BITSIZE = void 0;
4
4
  exports.CRYPTO_ASYM_BITSIZE = 4096;
5
5
  exports.CRYPTO_SYM_BITSIZE = 256;
6
6
  exports.CRYPTO_ASYM_BITSIZE_LIST = [1024, 2048, 4096];
7
7
  exports.CRYPTO_PUB_KEY_ENCODING = { type: 'spki', format: 'pem' };
8
8
  exports.CRYPTO_PRIV_KEY_ENCODING = { type: 'pkcs8', format: 'pem' };
9
+ // ---------------------------------------------------------------------------
10
+ // Field-level symmetric encryption (encryptText / decryptText).
11
+ //
12
+ // New scheme: AES-256-GCM (authenticated) with a random 12-byte IV per value.
13
+ // The stored value is `<version>:<base64(iv | authTag | ciphertext)>`, where the
14
+ // version prefix identifies the scheme and enables key rotation (a future `v2`
15
+ // can coexist with `v1`). The AES key is derived from the secret passphrase with
16
+ // scrypt (memory-hard) and cached per secret+version.
17
+ //
18
+ // Legacy scheme (produced by the previous CryptoJS.AES implementation) has NO
19
+ // version prefix: it is OpenSSL "Salted__" base64 (MD5 EVP_BytesToKey +
20
+ // AES-256-CBC). `decryptText` still reads it for backward compatibility.
21
+ // ---------------------------------------------------------------------------
22
+ exports.FIELD_ENC_VERSION = 'v1';
23
+ exports.FIELD_ENC_PREFIX = `${exports.FIELD_ENC_VERSION}:`;
24
+ exports.FIELD_ENC_ALGO = 'aes-256-gcm';
25
+ exports.FIELD_ENC_IV_BYTES = 12;
26
+ exports.FIELD_ENC_TAG_BYTES = 16;
27
+ exports.FIELD_ENC_KEY_BYTES = 32;
28
+ // scrypt KDF parameters used to derive the AES key from the secret passphrase.
29
+ // `N=16384,r=8,p=1` needs ~16 MB and is derived once per secret (then cached).
30
+ exports.FIELD_ENC_KDF_SALT = 'nxfb.field.v1';
31
+ exports.FIELD_ENC_KDF_N = 16384;
32
+ exports.FIELD_ENC_KDF_R = 8;
33
+ exports.FIELD_ENC_KDF_P = 1;
34
+ // Legacy CryptoJS/OpenSSL "Salted__" magic header (8 bytes, then 8-byte salt).
35
+ exports.LEGACY_ENC_MAGIC = 'Salted__';
9
36
  exports.CRYPTO_SYM_BITSIZE_MAP = {
10
37
  128: {
11
38
  keySize: 128,
@@ -38,7 +38,7 @@ export interface KeyAdditionalFieldAttrs {
38
38
  value: any;
39
39
  }
40
40
  export interface BlindIndexOptions {
41
- hasher?: any;
41
+ digest?: string;
42
42
  iterations?: number;
43
43
  keySize?: number;
44
44
  normalize?: boolean;
@@ -4,6 +4,8 @@ import { BlindIndexOptions } from './crypto.interfaces';
4
4
  import { SymmetricKey, AsymmetricKeyPair } from './crypto.classes';
5
5
  export declare class CryptoUtils {
6
6
  logger: any;
7
+ /** Cache of scrypt-derived AES keys, keyed by the secret passphrase. */
8
+ private readonly keyCache;
7
9
  constructor();
8
10
  /**
9
11
  * Generate a random Symmetric key based on AES bit size specified
@@ -52,48 +54,83 @@ export declare class CryptoUtils {
52
54
  */
53
55
  decypherAsym(privateKey: string, encData: string): Promise<string>;
54
56
  /**
55
- * Decrypt AES encrypted string using CryptoJs
57
+ * Derives (and caches) the AES-256 key for a secret passphrase via scrypt.
56
58
  *
57
- * @param { string } text AES encrypted string to be decrypted
59
+ * scrypt is memory-hard; the derivation runs once per distinct secret and the
60
+ * 32-byte result is cached, so per-field encrypt/decrypt stays cheap.
61
+ *
62
+ * @param secret the secret passphrase
63
+ * @returns the 32-byte AES key
64
+ */
65
+ private deriveFieldKey;
66
+ /**
67
+ * Decrypts a field value.
68
+ *
69
+ * Dispatches on the stored format: a `v1:`-prefixed value is AES-256-GCM
70
+ * (current scheme); an un-prefixed value is decrypted with the legacy
71
+ * CryptoJS/OpenSSL reader for backward compatibility. Returns `''` on missing
72
+ * input or on any decryption failure (matching the previous behaviour).
73
+ *
74
+ * @param { string } text AES encrypted string to be decrypted
58
75
  * @param { string } secret Secret string to decrypt value
59
76
  * @returns { string } Decrypted value
60
77
  */
61
78
  decryptText(text: string, secret: string): string;
62
79
  /**
63
- * Encrypt plain text string to AES using CryptoJS
80
+ * Encrypts a field value with the current scheme (AES-256-GCM).
64
81
  *
65
- * @param { string } text Plain text string to be AES encrypted
82
+ * Output is `v1:<base64(iv | authTag | ciphertext)>`. The version prefix keeps
83
+ * the value self-describing and enables future key rotation.
84
+ *
85
+ * @param { string } text Plain text string to be encrypted
66
86
  * @param { string } secret Secret string to encrypt value
67
87
  * @returns { string } Encrypted value
68
88
  */
69
89
  encryptText(text: string, secret: string): string;
70
90
  /**
71
- * Generate an md5 hash from given string
91
+ * Decrypts a current-scheme (`v1:`) AES-256-GCM value.
92
+ *
93
+ * @param text the `v1:`-prefixed ciphertext
94
+ * @param secret the secret passphrase
95
+ * @returns the decrypted UTF-8 plaintext
96
+ * @throws if the authentication tag does not verify (tampered/wrong key)
97
+ */
98
+ private decryptGcm;
99
+ /**
100
+ * Generates an md5 hash (hex) from the given string.
72
101
  *
73
102
  * @param {string} text
74
103
  */
75
104
  md5(text: string): string;
76
105
  /**
106
+ * Generates a sha256 hash (hex) from the given string.
77
107
  *
78
- * @param {*} text
79
- * @returns
108
+ * @param {string} text
109
+ * @returns {string} the hex digest
80
110
  */
81
111
  sha256(text: string): string;
82
112
  /**
113
+ * Generates a sha512 hash (hex) from the given string.
83
114
  *
84
115
  * @param {*} text
85
- * @returns
116
+ * @returns the hex digest
86
117
  */
87
118
  sha512(text: string): string;
88
119
  /**
89
- * Encrypt a text to PBKDF2 derivation function using given salt and keysize
120
+ * Derives a PBKDF2 hash (hex) used for searchable blind indexes.
121
+ *
122
+ * Byte-compatible with the previous CryptoJS implementation: `keySize` is in
123
+ * bits (default 256 → 32 output bytes) and `digest` selects the PRF hash.
124
+ * Existing `_bidx` values remain valid.
90
125
  *
91
126
  * @param { string } text
92
127
  * @param { string } salt
93
- * @param { number} keySize default 256
94
- * @return { string } Hashed value
128
+ * @param { number } keySize output size in bits (default 256)
129
+ * @param { string } digest PRF hash name (default `'sha256'`)
130
+ * @param { number } iterations iteration count (default 10000)
131
+ * @return { string } the hex digest
95
132
  */
96
- pbkdf2(text: string, salt: string, keySize?: number, hasher?: any, iterations?: number): string;
133
+ pbkdf2(text: string, salt: string, keySize?: number, digest?: string, iterations?: number): string;
97
134
  /**
98
135
  * Encrypt a text to PBKDF2 derivation function using given salt and keysize
99
136
  *
@@ -124,11 +161,17 @@ export declare class CryptoUtils {
124
161
  */
125
162
  createBlindIndexes(item: NxObject | undefined, keys: string[] | undefined, secret: string, opts?: BlindIndexOptions): void;
126
163
  /**
164
+ * Computes a searchable blind index (`<key>_bidx`) for a field.
127
165
  *
128
- * @param {*} item
129
- * @param {*} key
166
+ * Hashes the (optionally upper-cased) value with PBKDF2 so equality searches
167
+ * are possible over the encrypted field without exposing the plaintext.
168
+ *
169
+ * @param item the document, mutated in place with `<key>_bidx`
170
+ * @param key the field to index
171
+ * @param salt per-field salt (falls back to the field name)
172
+ * @param opts blind-index options (`digest`, `iterations`, `keySize`, `normalize`)
130
173
  */
131
- createBlindIndex(item: NxObject, key: string, salt: string, { hasher, iterations, keySize, normalize }?: BlindIndexOptions): void;
174
+ createBlindIndex(item: NxObject, key: string, salt: string, { digest, iterations, keySize, normalize }?: BlindIndexOptions): void;
132
175
  /**
133
176
  *
134
177
  * @param {*} item
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CryptoUtils = void 0;
7
7
  const crypto_1 = __importDefault(require("crypto"));
8
- const crypto_js_1 = __importDefault(require("crypto-js"));
9
8
  const stream_1 = require("stream");
10
9
  const constants_1 = require("../constants");
11
10
  const utils_1 = require("../utils");
@@ -15,9 +14,21 @@ const crypto_constants_2 = require("./crypto.constants");
15
14
  const crypto_constants_3 = require("./crypto.constants");
16
15
  const crypto_constants_4 = require("./crypto.constants");
17
16
  const crypto_constants_5 = require("./crypto.constants");
17
+ const crypto_constants_6 = require("./crypto.constants");
18
+ const crypto_constants_7 = require("./crypto.constants");
19
+ const crypto_constants_8 = require("./crypto.constants");
20
+ const crypto_constants_9 = require("./crypto.constants");
21
+ const crypto_constants_10 = require("./crypto.constants");
22
+ const crypto_constants_11 = require("./crypto.constants");
23
+ const crypto_constants_12 = require("./crypto.constants");
24
+ const crypto_constants_13 = require("./crypto.constants");
25
+ const crypto_legacy_1 = require("./crypto-legacy");
26
+ const crypto_legacy_2 = require("./crypto-legacy");
18
27
  const crypto_classes_1 = require("./crypto.classes");
19
28
  class CryptoUtils {
20
29
  constructor() {
30
+ /** Cache of scrypt-derived AES keys, keyed by the secret passphrase. */
31
+ this.keyCache = new Map();
21
32
  this.logger = constants_1.APP.logger;
22
33
  }
23
34
  /**
@@ -202,10 +213,37 @@ class CryptoUtils {
202
213
  });
203
214
  }
204
215
  // --------------------------------------------------------- CRYPTO-JS METHODS ---------------------------------------------------------------------------
216
+ // --------------------------------------------------------- SYMMETRIC FIELD ENCRYPTION ---------------------------------------------------------------
205
217
  /**
206
- * Decrypt AES encrypted string using CryptoJs
218
+ * Derives (and caches) the AES-256 key for a secret passphrase via scrypt.
207
219
  *
208
- * @param { string } text AES encrypted string to be decrypted
220
+ * scrypt is memory-hard; the derivation runs once per distinct secret and the
221
+ * 32-byte result is cached, so per-field encrypt/decrypt stays cheap.
222
+ *
223
+ * @param secret the secret passphrase
224
+ * @returns the 32-byte AES key
225
+ */
226
+ deriveFieldKey(secret) {
227
+ const cached = this.keyCache.get(secret);
228
+ if (cached)
229
+ return cached;
230
+ const key = crypto_1.default.scryptSync(secret, crypto_constants_10.FIELD_ENC_KDF_SALT, crypto_constants_9.FIELD_ENC_KEY_BYTES, {
231
+ N: crypto_constants_11.FIELD_ENC_KDF_N,
232
+ r: crypto_constants_12.FIELD_ENC_KDF_R,
233
+ p: crypto_constants_13.FIELD_ENC_KDF_P,
234
+ });
235
+ this.keyCache.set(secret, key);
236
+ return key;
237
+ }
238
+ /**
239
+ * Decrypts a field value.
240
+ *
241
+ * Dispatches on the stored format: a `v1:`-prefixed value is AES-256-GCM
242
+ * (current scheme); an un-prefixed value is decrypted with the legacy
243
+ * CryptoJS/OpenSSL reader for backward compatibility. Returns `''` on missing
244
+ * input or on any decryption failure (matching the previous behaviour).
245
+ *
246
+ * @param { string } text AES encrypted string to be decrypted
209
247
  * @param { string } secret Secret string to decrypt value
210
248
  * @returns { string } Decrypted value
211
249
  */
@@ -213,7 +251,12 @@ class CryptoUtils {
213
251
  if (!text || !secret)
214
252
  return '';
215
253
  try {
216
- return crypto_js_1.default.AES.decrypt(text, secret).toString(crypto_js_1.default.enc.Utf8);
254
+ if (text.startsWith(crypto_constants_6.FIELD_ENC_PREFIX))
255
+ return this.decryptGcm(text, secret);
256
+ if ((0, crypto_legacy_1.isLegacyCiphertext)(text))
257
+ return (0, crypto_legacy_2.decryptLegacyAes)(text, secret);
258
+ // Neither current nor legacy ciphertext: nothing safe to decrypt.
259
+ return '';
217
260
  }
218
261
  catch (e) {
219
262
  this.logger.error(e, (0, utils_1.logClassCtx)(this, 'decryptText'));
@@ -221,9 +264,12 @@ class CryptoUtils {
221
264
  }
222
265
  }
223
266
  /**
224
- * Encrypt plain text string to AES using CryptoJS
267
+ * Encrypts a field value with the current scheme (AES-256-GCM).
268
+ *
269
+ * Output is `v1:<base64(iv | authTag | ciphertext)>`. The version prefix keeps
270
+ * the value self-describing and enables future key rotation.
225
271
  *
226
- * @param { string } text Plain text string to be AES encrypted
272
+ * @param { string } text Plain text string to be encrypted
227
273
  * @param { string } secret Secret string to encrypt value
228
274
  * @returns { string } Encrypted value
229
275
  */
@@ -231,7 +277,12 @@ class CryptoUtils {
231
277
  if (!text || !secret)
232
278
  return '';
233
279
  try {
234
- return crypto_js_1.default.AES.encrypt(text, secret).toString();
280
+ const key = this.deriveFieldKey(secret);
281
+ const iv = crypto_1.default.randomBytes(crypto_constants_7.FIELD_ENC_IV_BYTES);
282
+ const cipher = crypto_1.default.createCipheriv(crypto_constants_6.FIELD_ENC_ALGO, key, iv);
283
+ const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
284
+ const authTag = cipher.getAuthTag();
285
+ return crypto_constants_6.FIELD_ENC_PREFIX + Buffer.concat([iv, authTag, ciphertext]).toString('base64');
235
286
  }
236
287
  catch (e) {
237
288
  this.logger.error(e, (0, utils_1.logClassCtx)(this, 'encryptText'));
@@ -239,47 +290,73 @@ class CryptoUtils {
239
290
  }
240
291
  }
241
292
  /**
242
- * Generate an md5 hash from given string
293
+ * Decrypts a current-scheme (`v1:`) AES-256-GCM value.
294
+ *
295
+ * @param text the `v1:`-prefixed ciphertext
296
+ * @param secret the secret passphrase
297
+ * @returns the decrypted UTF-8 plaintext
298
+ * @throws if the authentication tag does not verify (tampered/wrong key)
299
+ */
300
+ decryptGcm(text, secret) {
301
+ const raw = Buffer.from(text.slice(crypto_constants_6.FIELD_ENC_PREFIX.length), 'base64');
302
+ const iv = raw.subarray(0, crypto_constants_7.FIELD_ENC_IV_BYTES);
303
+ const authTag = raw.subarray(crypto_constants_7.FIELD_ENC_IV_BYTES, crypto_constants_7.FIELD_ENC_IV_BYTES + crypto_constants_8.FIELD_ENC_TAG_BYTES);
304
+ const ciphertext = raw.subarray(crypto_constants_7.FIELD_ENC_IV_BYTES + crypto_constants_8.FIELD_ENC_TAG_BYTES);
305
+ const key = this.deriveFieldKey(secret);
306
+ const decipher = crypto_1.default.createDecipheriv(crypto_constants_6.FIELD_ENC_ALGO, key, iv);
307
+ decipher.setAuthTag(authTag);
308
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
309
+ }
310
+ /**
311
+ * Generates an md5 hash (hex) from the given string.
243
312
  *
244
313
  * @param {string} text
245
314
  */
246
315
  md5(text) {
247
316
  if (!text)
248
317
  return '';
249
- return crypto_js_1.default.MD5(text).toString();
318
+ return crypto_1.default.createHash('md5').update(text).digest('hex');
250
319
  }
251
320
  /**
321
+ * Generates a sha256 hash (hex) from the given string.
252
322
  *
253
- * @param {*} text
254
- * @returns
323
+ * @param {string} text
324
+ * @returns {string} the hex digest
255
325
  */
256
326
  sha256(text) {
257
327
  if (!text)
258
328
  return '';
259
- return crypto_js_1.default.SHA256(text).toString();
329
+ return crypto_1.default.createHash('sha256').update(text).digest('hex');
260
330
  }
261
331
  /**
332
+ * Generates a sha512 hash (hex) from the given string.
262
333
  *
263
334
  * @param {*} text
264
- * @returns
335
+ * @returns the hex digest
265
336
  */
266
337
  sha512(text) {
267
338
  if (!text)
268
339
  return '';
269
- return crypto_js_1.default.SHA512(text).toString();
340
+ return crypto_1.default.createHash('sha512').update(text).digest('hex');
270
341
  }
271
342
  /**
272
- * Encrypt a text to PBKDF2 derivation function using given salt and keysize
343
+ * Derives a PBKDF2 hash (hex) used for searchable blind indexes.
344
+ *
345
+ * Byte-compatible with the previous CryptoJS implementation: `keySize` is in
346
+ * bits (default 256 → 32 output bytes) and `digest` selects the PRF hash.
347
+ * Existing `_bidx` values remain valid.
273
348
  *
274
349
  * @param { string } text
275
350
  * @param { string } salt
276
- * @param { number} keySize default 256
277
- * @return { string } Hashed value
351
+ * @param { number } keySize output size in bits (default 256)
352
+ * @param { string } digest PRF hash name (default `'sha256'`)
353
+ * @param { number } iterations iteration count (default 10000)
354
+ * @return { string } the hex digest
278
355
  */
279
- pbkdf2(text, salt, keySize = 256, hasher = crypto_js_1.default.algo.SHA256, iterations = 10000) {
356
+ pbkdf2(text, salt, keySize = 256, digest = 'sha256', iterations = 10000) {
280
357
  if (!text || !salt)
281
358
  return '';
282
- return crypto_js_1.default.PBKDF2(text, salt, { keySize: keySize / 32, hasher, iterations }).toString();
359
+ return crypto_1.default.pbkdf2Sync(text, salt, iterations, keySize / 8, digest).toString('hex');
283
360
  }
284
361
  /**
285
362
  * Encrypt a text to PBKDF2 derivation function using given salt and keysize
@@ -327,17 +404,23 @@ class CryptoUtils {
327
404
  keys.forEach(k => this.createBlindIndex(item, k, secret, opts));
328
405
  }
329
406
  /**
407
+ * Computes a searchable blind index (`<key>_bidx`) for a field.
330
408
  *
331
- * @param {*} item
332
- * @param {*} key
409
+ * Hashes the (optionally upper-cased) value with PBKDF2 so equality searches
410
+ * are possible over the encrypted field without exposing the plaintext.
411
+ *
412
+ * @param item the document, mutated in place with `<key>_bidx`
413
+ * @param key the field to index
414
+ * @param salt per-field salt (falls back to the field name)
415
+ * @param opts blind-index options (`digest`, `iterations`, `keySize`, `normalize`)
333
416
  */
334
- createBlindIndex(item, key, salt, { hasher, iterations, keySize, normalize } = {}) {
417
+ createBlindIndex(item, key, salt, { digest, iterations, keySize, normalize } = {}) {
335
418
  let value = (0, utils_1.getAttr)(item, key);
336
419
  if (!value)
337
420
  return;
338
421
  if (normalize)
339
422
  value = `${value}`.toUpperCase();
340
- (0, utils_1.setAttr)(item, `${key}_bidx`, this.pbkdf2(value, salt || key, keySize, hasher, iterations));
423
+ (0, utils_1.setAttr)(item, `${key}_bidx`, this.pbkdf2(value, salt || key, keySize, digest, iterations));
341
424
  }
342
425
  /**
343
426
  *
@@ -1,4 +1,6 @@
1
1
  export * from './crypto.interfaces';
2
2
  export * from './crypto.classes';
3
3
  export * from './crypto.utils';
4
+ export * from './crypto-legacy';
5
+ export * from './migrate-encrypted-fields';
4
6
  export * from './encryption.plugin';
@@ -17,4 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./crypto.interfaces"), exports);
18
18
  __exportStar(require("./crypto.classes"), exports);
19
19
  __exportStar(require("./crypto.utils"), exports);
20
+ __exportStar(require("./crypto-legacy"), exports);
21
+ __exportStar(require("./migrate-encrypted-fields"), exports);
20
22
  __exportStar(require("./encryption.plugin"), exports);
@@ -0,0 +1,55 @@
1
+ import { BaseModel } from '../base/base.model';
2
+ /**
3
+ * Reusable one-shot migration for legacy field-level encryption.
4
+ *
5
+ * The previous `CryptoJS.AES` implementation stored un-versioned OpenSSL
6
+ * `Salted__` ciphertexts. The current scheme is AES-256-GCM with a `v1:`
7
+ * version prefix. New writes migrate lazily (every `save` re-encrypts through the
8
+ * model setters), but records that are never re-saved stay in the legacy format.
9
+ *
10
+ * This engine bulk-migrates them: it reads documents at the **native driver
11
+ * level** (bypassing Mongoose getters), and for each configured field decrypts
12
+ * the legacy value and re-encrypts it with the current scheme, writing back at
13
+ * the **native driver level** too (bypassing setters, so the value is not
14
+ * double-encrypted). It is idempotent: already-`v1:` values are skipped, so it
15
+ * can be re-run safely.
16
+ *
17
+ * Because it operates through the standard nx-frame-be `BaseModel`, the same
18
+ * engine works for ANY project built on the framework: the caller only supplies
19
+ * the list of `{ model, fields }` targets (the fields that use the encrypting
20
+ * getter/setter in that project's schema).
21
+ */
22
+ /** A model plus the dotted paths of its encrypted string fields. */
23
+ export interface EncryptedFieldTarget {
24
+ model: BaseModel<any, any, any>;
25
+ fields: string[];
26
+ }
27
+ /** Options for {@link reEncryptLegacyFields}. */
28
+ export interface MigrateEncryptedFieldsOptions {
29
+ targets: EncryptedFieldTarget[];
30
+ secret?: string;
31
+ dryRun?: boolean;
32
+ batchSize?: number;
33
+ logger?: any;
34
+ }
35
+ /** Per-field counters for a migration run. */
36
+ export interface FieldMigrationStats {
37
+ migrated: number;
38
+ skipped: number;
39
+ errors: number;
40
+ }
41
+ /** Aggregated result of a migration run. */
42
+ export interface MigrateEncryptedFieldsResult {
43
+ scanned: number;
44
+ migrated: number;
45
+ skipped: number;
46
+ errors: number;
47
+ perModel: Record<string, Record<string, FieldMigrationStats>>;
48
+ }
49
+ /**
50
+ * Re-encrypts legacy-encrypted fields to the current AES-256-GCM scheme.
51
+ *
52
+ * @param opts the targets to migrate and run options
53
+ * @returns aggregated counters (also broken down per model and field)
54
+ */
55
+ export declare function reEncryptLegacyFields(opts: MigrateEncryptedFieldsOptions): Promise<MigrateEncryptedFieldsResult>;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
12
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
13
+ var m = o[Symbol.asyncIterator], i;
14
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
15
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
16
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.reEncryptLegacyFields = reEncryptLegacyFields;
20
+ const constants_1 = require("../constants");
21
+ const constants_2 = require("../constants");
22
+ const utils_1 = require("../utils");
23
+ const crypto_constants_1 = require("./crypto.constants");
24
+ const crypto_legacy_1 = require("./crypto-legacy");
25
+ const EMPTY_STATS = () => ({ migrated: 0, skipped: 0, errors: 0 });
26
+ /**
27
+ * Re-encrypts legacy-encrypted fields to the current AES-256-GCM scheme.
28
+ *
29
+ * @param opts the targets to migrate and run options
30
+ * @returns aggregated counters (also broken down per model and field)
31
+ */
32
+ function reEncryptLegacyFields(opts) {
33
+ return __awaiter(this, void 0, void 0, function* () {
34
+ var _a, e_1, _b, _c;
35
+ var _d, _e, _f, _g, _h;
36
+ const secret = (_f = (_e = (_d = opts.secret) !== null && _d !== void 0 ? _d : constants_1.APP.dataScr) !== null && _e !== void 0 ? _e : process.env.SECRET) !== null && _f !== void 0 ? _f : '';
37
+ const logger = (_g = opts.logger) !== null && _g !== void 0 ? _g : constants_1.APP.logger;
38
+ const dryRun = !!opts.dryRun;
39
+ const batchSize = (_h = opts.batchSize) !== null && _h !== void 0 ? _h : 500;
40
+ if (!secret)
41
+ throw new Error('reEncryptLegacyFields: no secret available (pass opts.secret or set APP.dataScr/SECRET)');
42
+ const result = {
43
+ scanned: 0,
44
+ migrated: 0,
45
+ skipped: 0,
46
+ errors: 0,
47
+ perModel: {},
48
+ };
49
+ for (const target of opts.targets) {
50
+ const modelName = target.model.name;
51
+ const collection = target.model.mgModel.collection;
52
+ result.perModel[modelName] = {};
53
+ for (const field of target.fields)
54
+ result.perModel[modelName][field] = EMPTY_STATS();
55
+ // Only fetch documents that carry at least one of the target fields.
56
+ const projection = target.fields.reduce((acc, f) => (acc[f] = 1, acc), { _id: 1 });
57
+ const cursor = collection.find({}, { projection, batchSize });
58
+ try {
59
+ for (var _j = true, cursor_1 = (e_1 = void 0, __asyncValues(cursor)), cursor_1_1; cursor_1_1 = yield cursor_1.next(), _a = cursor_1_1.done, !_a; _j = true) {
60
+ _c = cursor_1_1.value;
61
+ _j = false;
62
+ const doc = _c;
63
+ result.scanned++;
64
+ const update = {};
65
+ for (const field of target.fields) {
66
+ const stats = result.perModel[modelName][field];
67
+ const value = (0, utils_1.getAttr)(doc, field);
68
+ // Nothing to do: empty, or already migrated to the current scheme.
69
+ if (!value || typeof value !== 'string' || value.startsWith(crypto_constants_1.FIELD_ENC_PREFIX)) {
70
+ stats.skipped++;
71
+ result.skipped++;
72
+ continue;
73
+ }
74
+ // Only touch values we can confidently identify as legacy ciphertext.
75
+ if (!(0, crypto_legacy_1.isLegacyCiphertext)(value)) {
76
+ stats.skipped++;
77
+ result.skipped++;
78
+ continue;
79
+ }
80
+ try {
81
+ const plain = constants_2.cryptoUtils.decryptText(value, secret);
82
+ const reencoded = constants_2.cryptoUtils.encryptText(plain, secret);
83
+ if (!reencoded)
84
+ throw new Error('re-encryption produced an empty value');
85
+ // Use the DOTTED path as a literal `$set` key (e.g. `profile.email`) so
86
+ // MongoDB updates only that nested field and leaves its siblings intact.
87
+ // A nested object here would replace the whole sub-document.
88
+ update[field] = reencoded;
89
+ stats.migrated++;
90
+ result.migrated++;
91
+ }
92
+ catch (e) {
93
+ stats.errors++;
94
+ result.errors++;
95
+ logger.error(`reEncryptLegacyFields: ${modelName}.${field} on _id=${doc._id}: %o`, e);
96
+ }
97
+ }
98
+ if (!dryRun && Object.keys(update).length)
99
+ yield collection.updateOne({ _id: doc._id }, { $set: update });
100
+ }
101
+ }
102
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
103
+ finally {
104
+ try {
105
+ if (!_j && !_a && (_b = cursor_1.return)) yield _b.call(cursor_1);
106
+ }
107
+ finally { if (e_1) throw e_1.error; }
108
+ }
109
+ }
110
+ logger.info(`reEncryptLegacyFields: scanned=${result.scanned} migrated=${result.migrated} ` +
111
+ `skipped=${result.skipped} errors=${result.errors}${dryRun ? ' (dry-run)' : ''}`);
112
+ return result;
113
+ });
114
+ }
@@ -19,8 +19,14 @@ export declare function generateJwt(sub: string, jti: string, iss: string, data?
19
19
  */
20
20
  export declare function validateJwt(token: string, options?: jwt.VerifyOptions, secret?: jwt.Secret): string | jwt.Jwt | jwt.JwtPayload;
21
21
  /**
22
+ * Generates a cryptographically-strong random string.
22
23
  *
23
- * @param {*} size
24
+ * Uses a URL-safe base64 alphabet (`A-Za-z0-9-_`, no `+`/`/`/`=` padding) so the
25
+ * value is safe to drop into URLs, tokens and filenames. Enough random bytes are
26
+ * generated to fill `size` characters without truncating away entropy.
27
+ *
28
+ * @param size number of characters to return (default 21)
29
+ * @returns a random URL-safe string of length `size`
24
30
  */
25
31
  export declare function randomString(size?: number): string;
26
32
  /**
@@ -146,12 +146,20 @@ function validateJwt(token, options = {}, secret) {
146
146
  return jsonwebtoken_1.default.verify(token, secret, options);
147
147
  }
148
148
  /**
149
+ * Generates a cryptographically-strong random string.
149
150
  *
150
- * @param {*} size
151
+ * Uses a URL-safe base64 alphabet (`A-Za-z0-9-_`, no `+`/`/`/`=` padding) so the
152
+ * value is safe to drop into URLs, tokens and filenames. Enough random bytes are
153
+ * generated to fill `size` characters without truncating away entropy.
154
+ *
155
+ * @param size number of characters to return (default 21)
156
+ * @returns a random URL-safe string of length `size`
151
157
  */
152
158
  function randomString(size = 21) {
153
- return crypto_1.default.randomBytes(size)
154
- .toString('base64')
159
+ // base64url yields 4 chars per 3 bytes → ceil(3/4 * size) bytes cover `size` chars.
160
+ const bytes = Math.ceil((size * 3) / 4);
161
+ return crypto_1.default.randomBytes(bytes)
162
+ .toString('base64url')
155
163
  .slice(0, size);
156
164
  }
157
165
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextage/nx-frame-be",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -35,7 +35,6 @@
35
35
  "@aws-sdk/s3-request-presigner": "^3.1017.0",
36
36
  "@types/async": "^3.2.24",
37
37
  "@types/cookie-session": "^2.0.49",
38
- "@types/crypto-js": "^4.2.2",
39
38
  "@types/express": "^5.0.6",
40
39
  "@types/express-validator": "^3.0.0",
41
40
  "@types/graphql": "^14.5.0",
@@ -54,7 +53,6 @@
54
53
  "@types/uuid": "^9.0.8",
55
54
  "async": "^3.2.5",
56
55
  "cookie": "^1.1.1",
57
- "crypto-js": "^4.2.0",
58
56
  "express": "^5.2.1",
59
57
  "express-validator": "^7.1.0",
60
58
  "graphql": "^16.11.0",