@futdevpro/nts-dynamo 1.15.117 → 1.15.118

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,109 @@
1
+ import { DyFM_DataProperties } from '@futdevpro/fsm-dynamo';
2
+ import { DyFM_Crypto } from '@futdevpro/fsm-dynamo/crypto';
3
+
4
+ import { DyNTS_FieldEncryption_Util } from './field-encryption.util';
5
+
6
+ /**
7
+ * AT-REST field-encryption transzform-util tesztjei (AAA). Lefedi: opt-in no-op, create- ÉS
8
+ * update-alakú doc, roundtrip, legacy-plaintext, beágyazott (subObjectParams tömb), immutabilitás,
9
+ * idempotencia, kulcs-hiány.
10
+ */
11
+ describe('| DyNTS_FieldEncryption_Util', () => {
12
+ const KEY: string = 'db-content-crypt-key-abcdefgh';
13
+
14
+ // encrypt:true a `secret` mezőn; a `name` sima; a `steps[]` beágyazott, benne encrypt:true `prompt`.
15
+ const encProps: DyFM_DataProperties<any> = {
16
+ name: { key: 'name', type: 'string' },
17
+ secret: { key: 'secret', type: 'string', encrypt: true },
18
+ steps: {
19
+ key: 'steps', type: 'object[]',
20
+ subObjectParams: {
21
+ prompt: { key: 'prompt', type: 'string', encrypt: true },
22
+ label: { key: 'label', type: 'string' },
23
+ },
24
+ },
25
+ } as unknown as DyFM_DataProperties<any>;
26
+
27
+ const plainProps: DyFM_DataProperties<any> = {
28
+ name: { key: 'name', type: 'string' },
29
+ email: { key: 'email', type: 'string' },
30
+ } as unknown as DyFM_DataProperties<any>;
31
+
32
+ beforeEach(() => { process.env.FDP_CORE_DBCONTENT_CRYPT_KEY = KEY; });
33
+ afterEach(() => { delete process.env.FDP_CORE_DBCONTENT_CRYPT_KEY; });
34
+
35
+ it('| opt-in: encrypt:true mező nélküli modell → NO-OP (ugyanaz a referencia)', () => {
36
+ const data = { name: 'x', email: 'y' };
37
+ expect(DyNTS_FieldEncryption_Util.encryptDoc(data, plainProps)).toBe(data);
38
+ expect(DyNTS_FieldEncryption_Util.decryptDoc(data, plainProps)).toBe(data);
39
+ });
40
+
41
+ it('| CREATE-alak (nincs _id): encrypt:true mezőt envelope-ba teszi, a simát nem bántja', () => {
42
+ const data: any = { name: 'plain-name', secret: 'top-secret' };
43
+ const enc: any = DyNTS_FieldEncryption_Util.encryptDoc(data, encProps);
44
+
45
+ expect(DyFM_Crypto.isEncryptedDbValue(enc.secret)).toBe(true);
46
+ expect(enc.name).toBe('plain-name');
47
+ // immutabilitás: az eredeti NEM változott
48
+ expect(data.secret).toBe('top-secret');
49
+ });
50
+
51
+ it('| UPDATE-alak (van _id): ugyanúgy titkosít (a modifyData/findByIdAndUpdate útra)', () => {
52
+ const data: any = { _id: 'abc123', name: 'n', secret: 'updated-secret' };
53
+ const enc: any = DyNTS_FieldEncryption_Util.encryptDoc(data, encProps);
54
+
55
+ expect(enc._id).toBe('abc123');
56
+ expect(DyFM_Crypto.isEncryptedDbValue(enc.secret)).toBe(true);
57
+ expect(DyNTS_FieldEncryption_Util.decryptDoc(enc, encProps).secret).toBe('updated-secret');
58
+ });
59
+
60
+ it('| roundtrip: encrypt → decrypt visszaadja az eredetit', () => {
61
+ const data: any = { name: 'n', secret: 'a hosszú ő/ű prompt' };
62
+ const enc: any = DyNTS_FieldEncryption_Util.encryptDoc(data, encProps);
63
+ const dec: any = DyNTS_FieldEncryption_Util.decryptDoc(enc, encProps);
64
+
65
+ expect(dec.secret).toBe('a hosszú ő/ű prompt');
66
+ });
67
+
68
+ it('| beágyazott tömb (subObjectParams): a steps[].prompt titkosítva/visszafejtve, label érintetlen', () => {
69
+ const data: any = { name: 'n', steps: [ { prompt: 'p1', label: 'l1' }, { prompt: 'p2', label: 'l2' } ] };
70
+ const enc: any = DyNTS_FieldEncryption_Util.encryptDoc(data, encProps);
71
+
72
+ expect(DyFM_Crypto.isEncryptedDbValue(enc.steps[0].prompt)).toBe(true);
73
+ expect(enc.steps[0].label).toBe('l1');
74
+ expect(data.steps[0].prompt).toBe('p1'); // eredeti nem mutálódott
75
+
76
+ const dec: any = DyNTS_FieldEncryption_Util.decryptDoc(enc, encProps);
77
+ expect(dec.steps[0].prompt).toBe('p1');
78
+ expect(dec.steps[1].prompt).toBe('p2');
79
+ });
80
+
81
+ it('| decrypt: legacy plaintext (envelope nélkül) VÁLTOZATLAN', () => {
82
+ const data: any = { name: 'n', secret: 'legacy plaintext, még nem migrált' };
83
+ const dec: any = DyNTS_FieldEncryption_Util.decryptDoc(data, encProps);
84
+ expect(dec.secret).toBe('legacy plaintext, még nem migrált');
85
+ });
86
+
87
+ it('| idempotens: már-envelope értéket nem titkosít újra', () => {
88
+ const enc1: any = DyNTS_FieldEncryption_Util.encryptDoc({ secret: 's' }, encProps);
89
+ const enc2: any = DyNTS_FieldEncryption_Util.encryptDoc(enc1, encProps);
90
+ expect(enc2.secret).toBe(enc1.secret);
91
+ });
92
+
93
+ it('| üres/undefined encrypt:true értéket NEM titkosít (a crypto arra dobna)', () => {
94
+ const enc: any = DyNTS_FieldEncryption_Util.encryptDoc({ name: 'n', secret: '' }, encProps);
95
+ expect(enc.secret).toBe('');
96
+ });
97
+
98
+ it('| kulcs-hiány + encrypt:true mező → encrypt DOB (fail-loud misconfig)', () => {
99
+ delete process.env.FDP_CORE_DBCONTENT_CRYPT_KEY;
100
+ expect(() => DyNTS_FieldEncryption_Util.encryptDoc({ secret: 's' }, encProps)).toThrow();
101
+ });
102
+
103
+ it('| kulcs-hiány READ-en NEM dob (a read ne törjön) → az envelope visszafejtetlen marad', () => {
104
+ const enc: any = DyNTS_FieldEncryption_Util.encryptDoc({ secret: 's' }, encProps);
105
+ delete process.env.FDP_CORE_DBCONTENT_CRYPT_KEY;
106
+ const dec: any = DyNTS_FieldEncryption_Util.decryptDoc(enc, encProps);
107
+ expect(dec.secret).toBe(enc.secret); // envelope marad, nincs throw
108
+ });
109
+ });
@@ -0,0 +1,173 @@
1
+ import {
2
+ DyFM_DataProperties,
3
+ DyFM_Error,
4
+ } from '@futdevpro/fsm-dynamo';
5
+ import { DyFM_Crypto } from '@futdevpro/fsm-dynamo/crypto';
6
+
7
+ /**
8
+ * AT-REST field-encryption transzform (Option B, bedrock). A DBService a közös írási úton
9
+ * (`createData` / `modifyData`) a persist ELŐTT titkosítja, a read-post-processor
10
+ * (`stringifyDataId`) a load UTÁN visszafejti az `encrypt:true` mezőket.
11
+ *
12
+ * **App-rétegben fut, NEM mongoose-hook** — a `modifyData` `findByIdAndUpdate`-je megkerüli a
13
+ * mongoose settereket/pre-save-hookokat, ezért a transzform explicit itt történik.
14
+ *
15
+ * **Opt-in, default-off:** ha egy modell egyetlen mezője sem `encrypt:true`, a transzform tiszta
16
+ * NO-OP (ugyanaz az objektum-referencia, nincs klónozás, nincs viselkedés-változás, kulcs sem kell).
17
+ *
18
+ * **Legacy-plaintext biztonságos:** a read-transzform CSAK a `DYENC1:` envelope-értékeket fejti vissza
19
+ * (`isEncryptedDbValue`); a régi, még nem migrált plaintext értékek változatlanul jönnek vissza.
20
+ *
21
+ * Kulcs: `FDP_CORE_DBCONTENT_CRYPT_KEY` (Keystore/CI-env). Verzió: `FDP_CORE_DBCONTENT_CRYPT_KEY_VERSION`
22
+ * (default 1) — a kulcs-rotációhoz (a verzió az envelope-ban utazik).
23
+ */
24
+ export class DyNTS_FieldEncryption_Util {
25
+ static readonly ENV_KEY: string = 'FDP_CORE_DBCONTENT_CRYPT_KEY';
26
+ static readonly ENV_KEY_VERSION: string = 'FDP_CORE_DBCONTENT_CRYPT_KEY_VERSION';
27
+
28
+ /** `hasEncryptedFields` cache per properties-objektum (referencia szerint). */
29
+ private static readonly _hasEncCache: WeakMap<object, boolean> = new WeakMap();
30
+
31
+ /** A titkosító kulcs env-ből (üres string, ha nincs beállítva). */
32
+ static getKey(): string {
33
+ return process.env[this.ENV_KEY] ?? '';
34
+ }
35
+
36
+ /** Az aktuális kulcs-verzió (default 1). */
37
+ static getKeyVersion(): number {
38
+ const parsed: number = Number(process.env[this.ENV_KEY_VERSION]);
39
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
40
+ }
41
+
42
+ /**
43
+ * True, ha a properties-fa (rekurzívan a `subObjectParams`-on át) tartalmaz `encrypt:true` mezőt.
44
+ * Cache-elt (a properties-objektum immutábilis a modell-élettartam alatt).
45
+ */
46
+ static hasEncryptedFields(properties?: DyFM_DataProperties<any>): boolean {
47
+ if (!properties) {
48
+ return false;
49
+ }
50
+
51
+ const cached: boolean | undefined = this._hasEncCache.get(properties);
52
+ if (cached !== undefined) {
53
+ return cached;
54
+ }
55
+
56
+ let has: boolean = false;
57
+ for (const key in properties) {
58
+ const prop: any = properties[key]; // ccap-review-disable-line no-any-type
59
+ if (!prop) {
60
+ continue;
61
+ }
62
+ if (prop.encrypt === true) {
63
+ has = true;
64
+ break;
65
+ }
66
+ if (prop.subObjectParams && this.hasEncryptedFields(prop.subObjectParams)) {
67
+ has = true;
68
+ break;
69
+ }
70
+ }
71
+
72
+ this._hasEncCache.set(properties, has);
73
+ return has;
74
+ }
75
+
76
+ /**
77
+ * Titkosítja a doc `encrypt:true` mezőit persist előtt. **Klónt ad vissza** — az eredeti (hívó)
78
+ * objektumot NEM mutálja (különben a `this.data` ciphertextté válna). NO-OP, ha nincs enc-mező.
79
+ * @throws ha van enc-mező, de a kulcs env-var nincs beállítva (fail-loud misconfiguration).
80
+ */
81
+ static encryptDoc<T>(data: T, properties?: DyFM_DataProperties<any>): T {
82
+ if (data == null || !this.hasEncryptedFields(properties)) {
83
+ return data;
84
+ }
85
+
86
+ const key: string = this.getKey();
87
+ if (!key) {
88
+ throw new DyFM_Error({
89
+ status: 500,
90
+ errorCode: 'DyNTS-ENC-KEY-MISSING',
91
+ message:
92
+ `At-rest encryption: "${this.ENV_KEY}" env-var is not set, but a model field is encrypt:true. ` +
93
+ `Set the key in the Keystore/CI-env and redeploy.`,
94
+ });
95
+ }
96
+
97
+ return this._transform(data, properties as DyFM_DataProperties<any>, 'encrypt', key, this.getKeyVersion());
98
+ }
99
+
100
+ /**
101
+ * Visszafejti a doc `encrypt:true` mezőit load után. CSAK a `DYENC1:` envelope-értékeket bántja
102
+ * (legacy plaintext változatlan). NO-OP, ha nincs enc-mező. Kulcs hiányában read-en NEM dob
103
+ * (a read ne törjön el) — az envelope-értékek visszafejtetlenül maradnak (a hívó látja + a hiba loggolt).
104
+ */
105
+ static decryptDoc<T>(data: T, properties?: DyFM_DataProperties<any>): T {
106
+ if (data == null || !this.hasEncryptedFields(properties)) {
107
+ return data;
108
+ }
109
+
110
+ const key: string = this.getKey();
111
+ if (!key) {
112
+ return data;
113
+ }
114
+
115
+ return this._transform(data, properties as DyFM_DataProperties<any>, 'decrypt', key, this.getKeyVersion());
116
+ }
117
+
118
+ /**
119
+ * Rekurzív per-field walk: az `encrypt:true` mezőket titkosítja/visszafejti, a `subObjectParams`-os
120
+ * mezőkbe (objektum VAGY objektum-tömb) leszáll. Immutábilis: minden érintett konténert sekélyen
121
+ * klónoz (az eredetit nem mutálja); a nem-érintett ágakat referencia szerint megosztja (read-only).
122
+ */
123
+ private static _transform<T>(
124
+ value: T,
125
+ properties: DyFM_DataProperties<any>,
126
+ mode: 'encrypt' | 'decrypt',
127
+ key: string,
128
+ keyVersion: number,
129
+ ): T {
130
+ if (value == null || typeof value !== 'object') {
131
+ return value;
132
+ }
133
+
134
+ const result: any = Array.isArray(value) ? [ ...(value as any[]) ] : { ...(value as any) }; // ccap-review-disable-line no-any-type
135
+
136
+ for (const propKey in properties) {
137
+ const prop: any = properties[propKey]; // ccap-review-disable-line no-any-type
138
+ if (!prop) {
139
+ continue;
140
+ }
141
+
142
+ if (prop.encrypt === true) {
143
+ const current: any = result[propKey]; // ccap-review-disable-line no-any-type
144
+
145
+ if (mode === 'encrypt') {
146
+ // Üres/undefined/null-t NEM titkosítunk (a DyFM_Crypto ezekre dob); már-envelope-ot sem (idempotens).
147
+ if (current != null && current !== '' && !DyFM_Crypto.isEncryptedDbValue(current)) {
148
+ result[propKey] = DyFM_Crypto.encryptDbValue(current, key, keyVersion);
149
+ }
150
+ } else {
151
+ // CSAK envelope-ot fejtünk vissza (legacy plaintext változatlan).
152
+ if (DyFM_Crypto.isEncryptedDbValue(current)) {
153
+ result[propKey] = DyFM_Crypto.decryptDbValue(current, key);
154
+ }
155
+ }
156
+ } else if (prop.subObjectParams) {
157
+ const current: any = result[propKey]; // ccap-review-disable-line no-any-type
158
+
159
+ if (current != null && typeof current === 'object') {
160
+ if (Array.isArray(current)) {
161
+ result[propKey] = current.map(
162
+ (item: any): any => this._transform(item, prop.subObjectParams, mode, key, keyVersion), // ccap-review-disable-line no-any-type
163
+ );
164
+ } else {
165
+ result[propKey] = this._transform(current, prop.subObjectParams, mode, key, keyVersion);
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ return result as T;
172
+ }
173
+ }
@@ -0,0 +1,76 @@
1
+ import {
2
+ DyFM_DataModel_Params,
3
+ DyFM_Metadata,
4
+ DyFM_BasicProperty_Type,
5
+ } from '@futdevpro/fsm-dynamo';
6
+ import { DyFM_Crypto } from '@futdevpro/fsm-dynamo/crypto';
7
+ import * as mongoose from 'mongoose';
8
+
9
+ import { DyNTS_DBService } from './db.service';
10
+ import { DyNTS_FieldEncryption_Util } from '../../_collections/field-encryption.util';
11
+
12
+ class EncMeta extends DyFM_Metadata {
13
+ name: string = '';
14
+ secret: string = '';
15
+ }
16
+
17
+ const encParams: DyFM_DataModel_Params<EncMeta> = new DyFM_DataModel_Params<EncMeta>({
18
+ dataName: 'test_enc_data',
19
+ properties: {
20
+ name: { key: 'name', type: DyFM_BasicProperty_Type.string },
21
+ secret: { key: 'secret', type: DyFM_BasicProperty_Type.string, encrypt: true },
22
+ },
23
+ });
24
+
25
+ /**
26
+ * AT-REST encryption a DB-layer create/update útján. A `createData`/`modifyData` mongoose-hívása
27
+ * DB-kapcsolat nélkül nem futtatható végig (l. db.service.spec: mongoose.model getter-only), de:
28
+ * - a `findByIdAndUpdate` INSTANCE-property-ként spy-olható → az UPDATE-út (a findByIdAndUpdate
29
+ * hook-bypass caveat) igazolható;
30
+ * - a CREATE-út az AZONOS `encryptDoc`-ot hívja `new this.dataModel()` ELŐTT → spy-val igazolható,
31
+ * hogy a persist előtt lefut (a save DB nélkül elhasal, de már az encrypt UTÁN).
32
+ */
33
+ describe('| DyNTS_DBService at-rest field-encryption (create + update path)', () => {
34
+ let db: DyNTS_DBService<EncMeta>;
35
+ const KEY: string = 'db-content-crypt-key-abcdefgh';
36
+
37
+ beforeEach(() => {
38
+ process.env.FDP_CORE_DBCONTENT_CRYPT_KEY = KEY;
39
+ db = new DyNTS_DBService<EncMeta>(encParams);
40
+ });
41
+
42
+ afterEach(() => {
43
+ delete process.env.FDP_CORE_DBCONTENT_CRYPT_KEY;
44
+ delete (mongoose.models as Record<string, unknown>)['test_enc_data'];
45
+ });
46
+
47
+ it('| UPDATE: modifyData → a findByIdAndUpdate CIPHERTEXT-et kap (hook-bypass ellenére), a return plaintext', async () => {
48
+ const spy: jasmine.Spy = spyOn(db['dataModel'], 'findByIdAndUpdate').and.returnValue(Promise.resolve({}) as never);
49
+
50
+ const result: EncMeta = await db.modifyData({ _id: 'id1', name: 'n', secret: 'plain-secret' } as EncMeta, 'issuer');
51
+
52
+ const persisted: any = spy.calls.mostRecent().args[1]; // ccap-review-disable-line no-any-type
53
+ expect(DyFM_Crypto.isEncryptedDbValue(persisted.secret)).toBe(true); // DB-be ciphertext ment
54
+ expect(persisted.name).toBe('n'); // sima mező érintetlen
55
+ expect(result.secret).toBe('plain-secret'); // a hívó plaintextet kap vissza
56
+ });
57
+
58
+ it('| CREATE: createData a persist (new dataModel/save) ELŐTT titkosít (encryptDoc a create-adattal)', async () => {
59
+ const encSpy: jasmine.Spy = spyOn(DyNTS_FieldEncryption_Util, 'encryptDoc').and.callThrough();
60
+
61
+ // A .save() DB-kapcsolat nélkül buffer-elne (~10s). Kikapcsoljuk a bufferelést → GYORS fail.
62
+ const prevBuffer: boolean | undefined = mongoose.get('bufferCommands') as boolean | undefined;
63
+ mongoose.set('bufferCommands', false);
64
+ try {
65
+ await db.createData({ name: 'n', secret: 'plain-secret' } as EncMeta, 'issuer');
66
+ } catch {
67
+ // A .save() DB nélkül elhasal — a lényeg, hogy az encrypt-transzform ELŐTTE lefutott.
68
+ } finally {
69
+ mongoose.set('bufferCommands', prevBuffer ?? true);
70
+ }
71
+
72
+ expect(encSpy).toHaveBeenCalled();
73
+ const passed: any = encSpy.calls.mostRecent().args[0]; // ccap-review-disable-line no-any-type
74
+ expect(passed.secret).toBe('plain-secret');
75
+ });
76
+ });
@@ -16,6 +16,7 @@ import {
16
16
  DyFM_Object
17
17
  } from '@futdevpro/fsm-dynamo';
18
18
  import { DyNTS_archiveSuffix } from '../../_collections/archive.util';
19
+ import { DyNTS_FieldEncryption_Util } from '../../_collections/field-encryption.util';
19
20
  import { DyNTS_DBUpdate } from '../../_models/types/db-update.type';
20
21
  import { DyNTS_DBQueryOptions } from '../../_models/interfaces/db-query-options.interface';
21
22
  import { DyNTS_global_settings } from '../../_collections/global-settings.const';
@@ -100,7 +101,11 @@ export class DyNTS_DBService<T extends DyFM_Metadata> {
100
101
  data.__createdBy = issuer;
101
102
  data.__lastModifiedBy = issuer;
102
103
 
103
- const dataModel = new this.dataModel(data);
104
+ // AT-REST field-encryption (opt-in): az encrypt:true mezőket a persist ELŐTT titkosítjuk, egy
105
+ // KLÓNON (a hívó `data`-ja plaintext marad). NO-OP, ha nincs encrypt:true mező.
106
+ const encData: T = DyNTS_FieldEncryption_Util.encryptDoc(data, this.dataParams.properties);
107
+
108
+ const dataModel = new this.dataModel(encData);
104
109
  const newData: T = await dataModel.save().then((res): T => {
105
110
  if (res) {
106
111
  return res?.toObject() as T;
@@ -157,12 +162,18 @@ export class DyNTS_DBService<T extends DyFM_Metadata> {
157
162
  data.__lastModifiedBy = issuer;
158
163
  }
159
164
 
160
- /**
161
- * EZ A SZAR TELJESEN SZAR, nem friss, nem a db-be mentett adatokat ad vissza,
162
- * átír random value-kat össze vissza, WTF
165
+ // AT-REST field-encryption (opt-in): az encrypt:true mezőket a persist ELŐTT titkosítjuk, egy
166
+ // KLÓNON. KRITIKUS: a `findByIdAndUpdate` MEGKERÜLI a mongoose settereket/pre-save-hookokat, ezért
167
+ // a titkosítás itt, app-rétegben történik (nem mongoose-hookban). A hívó `data`-ja plaintext marad,
168
+ // így a `stringifyDataId(data)` alant no-op decrypttel a plaintextet adja vissza. NO-OP enc-mező nélkül.
169
+ const encData: T = DyNTS_FieldEncryption_Util.encryptDoc(data, this.dataParams.properties);
170
+
171
+ /**
172
+ * EZ A SZAR TELJESEN SZAR, nem friss, nem a db-be mentett adatokat ad vissza,
173
+ * átír random value-kat össze vissza, WTF
163
174
  * */
164
- /* let newData: T = */
165
- await this.dataModel.findByIdAndUpdate(data._id, data)/* .then((res) => {
175
+ /* let newData: T = */
176
+ await this.dataModel.findByIdAndUpdate(data._id, encData)/* .then((res) => {
166
177
  if (res) {
167
178
  //return res?.toObject() as T;
168
179
  } else {
@@ -1277,6 +1288,11 @@ export class DyNTS_DBService<T extends DyFM_Metadata> {
1277
1288
  // PRIVATE FUNCTIONS
1278
1289
 
1279
1290
  private stringifyDataId(data: T, fnName: string): T {
1291
+ // AT-REST field-decryption (opt-in): ez a KÖZÖS read+write post-processor (minden read + a
1292
+ // create/modify EREDMÉNYE is átmegy rajta), ezért itt fejtjük vissza az encrypt:true envelope-
1293
+ // mezőket load után. NO-OP, ha nincs encrypt:true mező (ugyanaz a referencia — nulla változás).
1294
+ data = DyNTS_FieldEncryption_Util.decryptDoc(data, this.dataParams.properties);
1295
+
1280
1296
  if (data?._id && (typeof data._id !== 'string' || typeof data._id === 'object')) {
1281
1297
  data._id = `${data._id}`;
1282
1298