@djangocfg/crypto 2.1.111
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 +161 -0
- package/dist/index.cjs +267 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +297 -0
- package/dist/index.d.ts +297 -0
- package/dist/index.mjs +246 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react.cjs +300 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +234 -0
- package/dist/react.d.ts +234 -0
- package/dist/react.mjs +279 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +79 -0
- package/src/decryption.ts +271 -0
- package/src/index.ts +66 -0
- package/src/key-derivation.ts +168 -0
- package/src/react/hooks.ts +236 -0
- package/src/react/index.ts +21 -0
- package/src/types.ts +159 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @djangocfg/crypto
|
|
3
|
+
*
|
|
4
|
+
* Client-side AES-256-GCM decryption for Django-CFG encrypted API responses.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Basic usage - decrypt a single field
|
|
11
|
+
* import { createDecryptionClient } from '@djangocfg/crypto';
|
|
12
|
+
*
|
|
13
|
+
* const crypto = await createDecryptionClient({
|
|
14
|
+
* secretKey: 'your-django-secret-key',
|
|
15
|
+
* userId: 123 // optional, for per-user encryption
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const response = await fetch('/api/products/?encrypt=true');
|
|
19
|
+
* const data = await crypto.decryptObject(await response.json());
|
|
20
|
+
* console.log(data.price); // decrypted value
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // React usage
|
|
26
|
+
* import { useDecrypt } from '@djangocfg/crypto/react';
|
|
27
|
+
*
|
|
28
|
+
* function ProductPrice({ product }) {
|
|
29
|
+
* const { data, isLoading } = useDecrypt(product, {
|
|
30
|
+
* secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* if (isLoading) return <Skeleton />;
|
|
34
|
+
* return <span>{data.price}</span>;
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// Types
|
|
40
|
+
export type {
|
|
41
|
+
EncryptedField,
|
|
42
|
+
EncryptedResponse,
|
|
43
|
+
DecryptionConfig,
|
|
44
|
+
DecryptionResult,
|
|
45
|
+
DecryptionError,
|
|
46
|
+
} from './types';
|
|
47
|
+
|
|
48
|
+
export { isEncryptedField, isEncryptedResponse } from './types';
|
|
49
|
+
|
|
50
|
+
// Key derivation
|
|
51
|
+
export {
|
|
52
|
+
deriveKey,
|
|
53
|
+
deriveKeyBytes,
|
|
54
|
+
deriveKeyFromConfig,
|
|
55
|
+
buildSalt,
|
|
56
|
+
} from './key-derivation';
|
|
57
|
+
|
|
58
|
+
// Decryption
|
|
59
|
+
export {
|
|
60
|
+
decryptAES256GCM,
|
|
61
|
+
decryptField,
|
|
62
|
+
decryptResponse,
|
|
63
|
+
decryptObject,
|
|
64
|
+
createDecryptionClient,
|
|
65
|
+
safeDecrypt,
|
|
66
|
+
} from './decryption';
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PBKDF2 key derivation using Web Crypto API.
|
|
3
|
+
*
|
|
4
|
+
* Matches Django-CFG backend key derivation for decryption compatibility.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Derive an encryption key using PBKDF2.
|
|
9
|
+
*
|
|
10
|
+
* Uses Web Crypto API for secure key derivation that matches
|
|
11
|
+
* the Django-CFG backend implementation.
|
|
12
|
+
*
|
|
13
|
+
* @param password - The password/secret key to derive from
|
|
14
|
+
* @param salt - Salt bytes for key derivation
|
|
15
|
+
* @param iterations - Number of PBKDF2 iterations (default: 100000)
|
|
16
|
+
* @param keyLength - Desired key length in bytes (default: 32 for AES-256)
|
|
17
|
+
* @returns Promise resolving to derived key as CryptoKey
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const salt = new TextEncoder().encode('my-salt');
|
|
22
|
+
* const key = await deriveKey('secret', salt, 100000);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function deriveKey(
|
|
26
|
+
password: string,
|
|
27
|
+
salt: Uint8Array,
|
|
28
|
+
iterations: number = 100000,
|
|
29
|
+
keyLength: number = 32
|
|
30
|
+
): Promise<CryptoKey> {
|
|
31
|
+
const encoder = new TextEncoder();
|
|
32
|
+
const passwordBuffer = encoder.encode(password);
|
|
33
|
+
|
|
34
|
+
// Import password as raw key material
|
|
35
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
36
|
+
'raw',
|
|
37
|
+
passwordBuffer,
|
|
38
|
+
'PBKDF2',
|
|
39
|
+
false,
|
|
40
|
+
['deriveBits', 'deriveKey']
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Derive AES-GCM key using PBKDF2
|
|
44
|
+
return crypto.subtle.deriveKey(
|
|
45
|
+
{
|
|
46
|
+
name: 'PBKDF2',
|
|
47
|
+
salt: salt.buffer as ArrayBuffer,
|
|
48
|
+
iterations: iterations,
|
|
49
|
+
hash: 'SHA-256',
|
|
50
|
+
},
|
|
51
|
+
keyMaterial,
|
|
52
|
+
{ name: 'AES-GCM', length: keyLength * 8 },
|
|
53
|
+
false,
|
|
54
|
+
['decrypt']
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive raw key bytes using PBKDF2.
|
|
60
|
+
*
|
|
61
|
+
* @param password - The password/secret key to derive from
|
|
62
|
+
* @param salt - Salt bytes for key derivation
|
|
63
|
+
* @param iterations - Number of PBKDF2 iterations (default: 100000)
|
|
64
|
+
* @param keyLength - Desired key length in bytes (default: 32 for AES-256)
|
|
65
|
+
* @returns Promise resolving to derived key as Uint8Array
|
|
66
|
+
*/
|
|
67
|
+
export async function deriveKeyBytes(
|
|
68
|
+
password: string,
|
|
69
|
+
salt: Uint8Array,
|
|
70
|
+
iterations: number = 100000,
|
|
71
|
+
keyLength: number = 32
|
|
72
|
+
): Promise<Uint8Array> {
|
|
73
|
+
const encoder = new TextEncoder();
|
|
74
|
+
const passwordBuffer = encoder.encode(password);
|
|
75
|
+
|
|
76
|
+
// Import password as raw key material
|
|
77
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
78
|
+
'raw',
|
|
79
|
+
passwordBuffer,
|
|
80
|
+
'PBKDF2',
|
|
81
|
+
false,
|
|
82
|
+
['deriveBits']
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Derive raw bits
|
|
86
|
+
const keyBits = await crypto.subtle.deriveBits(
|
|
87
|
+
{
|
|
88
|
+
name: 'PBKDF2',
|
|
89
|
+
salt: salt.buffer as ArrayBuffer,
|
|
90
|
+
iterations: iterations,
|
|
91
|
+
hash: 'SHA-256',
|
|
92
|
+
},
|
|
93
|
+
keyMaterial,
|
|
94
|
+
keyLength * 8
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return new Uint8Array(keyBits);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build a deterministic salt from context components.
|
|
102
|
+
*
|
|
103
|
+
* Matches Django-CFG backend salt generation for key derivation.
|
|
104
|
+
*
|
|
105
|
+
* @param keyPrefix - Key prefix (default: "djangocfg_encryption")
|
|
106
|
+
* @param userId - Optional user ID for per-user keys
|
|
107
|
+
* @param sessionId - Optional session ID for per-session keys
|
|
108
|
+
* @returns Salt as Uint8Array (first 16 bytes of SHA-256 hash)
|
|
109
|
+
*/
|
|
110
|
+
export async function buildSalt(
|
|
111
|
+
keyPrefix: string = 'djangocfg_encryption',
|
|
112
|
+
userId?: string | number,
|
|
113
|
+
sessionId?: string
|
|
114
|
+
): Promise<Uint8Array> {
|
|
115
|
+
const parts = [keyPrefix];
|
|
116
|
+
|
|
117
|
+
if (sessionId) {
|
|
118
|
+
parts.push(`session:${sessionId}`);
|
|
119
|
+
} else if (userId !== undefined) {
|
|
120
|
+
parts.push(`user:${userId}`);
|
|
121
|
+
} else {
|
|
122
|
+
parts.push('global');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const saltInput = parts.join(':');
|
|
126
|
+
const encoder = new TextEncoder();
|
|
127
|
+
const inputBuffer = encoder.encode(saltInput);
|
|
128
|
+
|
|
129
|
+
// SHA-256 hash and take first 16 bytes
|
|
130
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', inputBuffer);
|
|
131
|
+
return new Uint8Array(hashBuffer).slice(0, 16);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Derive encryption key from Django-CFG config.
|
|
136
|
+
*
|
|
137
|
+
* Convenience function that matches backend key derivation.
|
|
138
|
+
*
|
|
139
|
+
* @param config - Configuration object with secretKey and optional context
|
|
140
|
+
* @returns Promise resolving to CryptoKey for decryption
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const key = await deriveKeyFromConfig({
|
|
145
|
+
* secretKey: 'django-secret-key',
|
|
146
|
+
* userId: 123,
|
|
147
|
+
* iterations: 100000
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export async function deriveKeyFromConfig(config: {
|
|
152
|
+
secretKey: string;
|
|
153
|
+
userId?: string | number;
|
|
154
|
+
sessionId?: string;
|
|
155
|
+
iterations?: number;
|
|
156
|
+
keyPrefix?: string;
|
|
157
|
+
}): Promise<CryptoKey> {
|
|
158
|
+
const {
|
|
159
|
+
secretKey,
|
|
160
|
+
userId,
|
|
161
|
+
sessionId,
|
|
162
|
+
iterations = 100000,
|
|
163
|
+
keyPrefix = 'djangocfg_encryption',
|
|
164
|
+
} = config;
|
|
165
|
+
|
|
166
|
+
const salt = await buildSalt(keyPrefix, userId, sessionId);
|
|
167
|
+
return deriveKey(secretKey, salt, iterations);
|
|
168
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks for Django-CFG encryption.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import type { DecryptionConfig, EncryptedField } from '../types';
|
|
7
|
+
import { isEncryptedField } from '../types';
|
|
8
|
+
import { createDecryptionClient, decryptObject } from '../decryption';
|
|
9
|
+
import { deriveKeyFromConfig } from '../key-derivation';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook state for decryption operations.
|
|
13
|
+
*/
|
|
14
|
+
interface UseDecryptState<T> {
|
|
15
|
+
/** Decrypted data */
|
|
16
|
+
data: T | undefined;
|
|
17
|
+
/** Loading state */
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
/** Error if decryption failed */
|
|
20
|
+
error: Error | undefined;
|
|
21
|
+
/** Whether data has been decrypted */
|
|
22
|
+
isDecrypted: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook to decrypt data on mount or when dependencies change.
|
|
27
|
+
*
|
|
28
|
+
* @param encryptedData - Data potentially containing encrypted fields
|
|
29
|
+
* @param config - Decryption configuration
|
|
30
|
+
* @returns Decrypted data state
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* function ProductPrice({ product }: { product: Product }) {
|
|
35
|
+
* const { data, isLoading, error } = useDecrypt(product, {
|
|
36
|
+
* secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!,
|
|
37
|
+
* userId: user.id
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* if (isLoading) return <Skeleton />;
|
|
41
|
+
* if (error) return <ErrorMessage error={error} />;
|
|
42
|
+
* return <span>{data.price}</span>;
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useDecrypt<T>(
|
|
47
|
+
encryptedData: unknown,
|
|
48
|
+
config: DecryptionConfig
|
|
49
|
+
): UseDecryptState<T> {
|
|
50
|
+
const [state, setState] = useState<UseDecryptState<T>>({
|
|
51
|
+
data: undefined,
|
|
52
|
+
isLoading: true,
|
|
53
|
+
error: undefined,
|
|
54
|
+
isDecrypted: false,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Cache the key to avoid re-deriving on every render
|
|
58
|
+
const keyRef = useRef<CryptoKey | null>(null);
|
|
59
|
+
const configRef = useRef(config);
|
|
60
|
+
|
|
61
|
+
// Check if config changed
|
|
62
|
+
const configChanged =
|
|
63
|
+
configRef.current.secretKey !== config.secretKey ||
|
|
64
|
+
configRef.current.userId !== config.userId ||
|
|
65
|
+
configRef.current.sessionId !== config.sessionId;
|
|
66
|
+
|
|
67
|
+
if (configChanged) {
|
|
68
|
+
configRef.current = config;
|
|
69
|
+
keyRef.current = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
let cancelled = false;
|
|
74
|
+
|
|
75
|
+
async function decrypt() {
|
|
76
|
+
try {
|
|
77
|
+
setState((s) => ({ ...s, isLoading: true, error: undefined }));
|
|
78
|
+
|
|
79
|
+
// Get or derive key
|
|
80
|
+
if (!keyRef.current) {
|
|
81
|
+
keyRef.current = await deriveKeyFromConfig(config);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const decrypted = await decryptObject<T>(encryptedData, keyRef.current);
|
|
85
|
+
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
setState({
|
|
88
|
+
data: decrypted,
|
|
89
|
+
isLoading: false,
|
|
90
|
+
error: undefined,
|
|
91
|
+
isDecrypted: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (!cancelled) {
|
|
96
|
+
setState({
|
|
97
|
+
data: undefined,
|
|
98
|
+
isLoading: false,
|
|
99
|
+
error: err instanceof Error ? err : new Error('Decryption failed'),
|
|
100
|
+
isDecrypted: false,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
decrypt();
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [encryptedData, config.secretKey, config.userId, config.sessionId]);
|
|
112
|
+
|
|
113
|
+
return state;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Hook to create a memoized decryption client.
|
|
118
|
+
*
|
|
119
|
+
* @param config - Decryption configuration
|
|
120
|
+
* @returns Decryption client or undefined while loading
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* function App() {
|
|
125
|
+
* const crypto = useDecryptionClient({
|
|
126
|
+
* secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
129
|
+
* const handleFetch = async () => {
|
|
130
|
+
* const response = await fetch('/api/products/?encrypt=true');
|
|
131
|
+
* const data = await response.json();
|
|
132
|
+
* const decrypted = await crypto?.decryptObject(data);
|
|
133
|
+
* };
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function useDecryptionClient(config: DecryptionConfig) {
|
|
138
|
+
const [client, setClient] = useState<Awaited<
|
|
139
|
+
ReturnType<typeof createDecryptionClient>
|
|
140
|
+
> | null>(null);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
createDecryptionClient(config).then(setClient);
|
|
144
|
+
}, [config.secretKey, config.userId, config.sessionId]);
|
|
145
|
+
|
|
146
|
+
return client;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Hook for lazy decryption with manual trigger.
|
|
151
|
+
*
|
|
152
|
+
* @param config - Decryption configuration
|
|
153
|
+
* @returns Decrypt function and state
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* function LazyProduct({ product }: { product: Product }) {
|
|
158
|
+
* const { decrypt, data, isLoading } = useLazyDecrypt({
|
|
159
|
+
* secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!
|
|
160
|
+
* });
|
|
161
|
+
*
|
|
162
|
+
* return (
|
|
163
|
+
* <div>
|
|
164
|
+
* <button onClick={() => decrypt(product)}>Show Price</button>
|
|
165
|
+
* {isLoading && <Spinner />}
|
|
166
|
+
* {data && <span>{data.price}</span>}
|
|
167
|
+
* </div>
|
|
168
|
+
* );
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export function useLazyDecrypt<T>(config: DecryptionConfig) {
|
|
173
|
+
const [state, setState] = useState<UseDecryptState<T>>({
|
|
174
|
+
data: undefined,
|
|
175
|
+
isLoading: false,
|
|
176
|
+
error: undefined,
|
|
177
|
+
isDecrypted: false,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const keyRef = useRef<CryptoKey | null>(null);
|
|
181
|
+
|
|
182
|
+
const decrypt = useCallback(
|
|
183
|
+
async (encryptedData: unknown): Promise<T | undefined> => {
|
|
184
|
+
try {
|
|
185
|
+
setState((s) => ({ ...s, isLoading: true, error: undefined }));
|
|
186
|
+
|
|
187
|
+
if (!keyRef.current) {
|
|
188
|
+
keyRef.current = await deriveKeyFromConfig(config);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const decrypted = await decryptObject<T>(encryptedData, keyRef.current);
|
|
192
|
+
|
|
193
|
+
setState({
|
|
194
|
+
data: decrypted,
|
|
195
|
+
isLoading: false,
|
|
196
|
+
error: undefined,
|
|
197
|
+
isDecrypted: true,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return decrypted;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const error =
|
|
203
|
+
err instanceof Error ? err : new Error('Decryption failed');
|
|
204
|
+
setState({
|
|
205
|
+
data: undefined,
|
|
206
|
+
isLoading: false,
|
|
207
|
+
error,
|
|
208
|
+
isDecrypted: false,
|
|
209
|
+
});
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
[config.secretKey, config.userId, config.sessionId]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const reset = useCallback(() => {
|
|
217
|
+
setState({
|
|
218
|
+
data: undefined,
|
|
219
|
+
isLoading: false,
|
|
220
|
+
error: undefined,
|
|
221
|
+
isDecrypted: false,
|
|
222
|
+
});
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
return { ...state, decrypt, reset };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Hook to check if a value needs decryption.
|
|
230
|
+
*
|
|
231
|
+
* @param value - Value to check
|
|
232
|
+
* @returns Whether the value is encrypted
|
|
233
|
+
*/
|
|
234
|
+
export function useIsEncrypted(value: unknown): boolean {
|
|
235
|
+
return useMemo(() => isEncryptedField(value), [value]);
|
|
236
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React integration for @djangocfg/crypto.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
useDecrypt,
|
|
9
|
+
useDecryptionClient,
|
|
10
|
+
useLazyDecrypt,
|
|
11
|
+
useIsEncrypted,
|
|
12
|
+
} from './hooks';
|
|
13
|
+
|
|
14
|
+
// Re-export types for convenience
|
|
15
|
+
export type {
|
|
16
|
+
DecryptionConfig,
|
|
17
|
+
DecryptionResult,
|
|
18
|
+
DecryptionError,
|
|
19
|
+
EncryptedField,
|
|
20
|
+
EncryptedResponse,
|
|
21
|
+
} from '../types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript interfaces for Django-CFG encryption.
|
|
3
|
+
*
|
|
4
|
+
* These types match the encrypted response format from Django-CFG backend.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Encrypted field envelope returned by Django-CFG API.
|
|
9
|
+
*
|
|
10
|
+
* When a serializer field is encrypted, it returns this structure
|
|
11
|
+
* instead of the plain value.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```json
|
|
15
|
+
* {
|
|
16
|
+
* "encrypted": true,
|
|
17
|
+
* "field": "price",
|
|
18
|
+
* "algorithm": "AES-256-GCM",
|
|
19
|
+
* "iv": "base64...",
|
|
20
|
+
* "data": "base64...",
|
|
21
|
+
* "auth_tag": "base64..."
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export interface EncryptedField {
|
|
26
|
+
/** Always true for encrypted fields */
|
|
27
|
+
encrypted: true;
|
|
28
|
+
/** Field name that was encrypted */
|
|
29
|
+
field?: string;
|
|
30
|
+
/** Encryption algorithm used */
|
|
31
|
+
algorithm: 'AES-256-GCM' | 'AES-256-CBC';
|
|
32
|
+
/** Base64-encoded initialization vector */
|
|
33
|
+
iv: string;
|
|
34
|
+
/** Base64-encoded ciphertext */
|
|
35
|
+
data: string;
|
|
36
|
+
/** Base64-encoded authentication tag (GCM only) */
|
|
37
|
+
auth_tag: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Full encrypted response envelope.
|
|
42
|
+
*
|
|
43
|
+
* When response-level encryption is enabled, the entire response
|
|
44
|
+
* body is wrapped in this structure.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```json
|
|
48
|
+
* {
|
|
49
|
+
* "encrypted": true,
|
|
50
|
+
* "algorithm": "AES-256-GCM",
|
|
51
|
+
* "salt": "base64...",
|
|
52
|
+
* "iv": "base64...",
|
|
53
|
+
* "data": "base64...",
|
|
54
|
+
* "auth_tag": "base64..."
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export interface EncryptedResponse {
|
|
59
|
+
/** Always true for encrypted responses */
|
|
60
|
+
encrypted: true;
|
|
61
|
+
/** Encryption algorithm used */
|
|
62
|
+
algorithm: 'AES-256-GCM' | 'AES-256-CBC';
|
|
63
|
+
/** Base64-encoded salt for key derivation */
|
|
64
|
+
salt: string;
|
|
65
|
+
/** Base64-encoded initialization vector */
|
|
66
|
+
iv: string;
|
|
67
|
+
/** Base64-encoded ciphertext */
|
|
68
|
+
data: string;
|
|
69
|
+
/** Base64-encoded authentication tag (GCM only) */
|
|
70
|
+
auth_tag: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configuration for the decryption client.
|
|
75
|
+
*/
|
|
76
|
+
export interface DecryptionConfig {
|
|
77
|
+
/**
|
|
78
|
+
* Secret key for key derivation.
|
|
79
|
+
* Should match the Django SECRET_KEY or a derived key.
|
|
80
|
+
*/
|
|
81
|
+
secretKey: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* User ID for per-user key derivation (optional).
|
|
85
|
+
* When provided, keys are derived per-user for isolation.
|
|
86
|
+
*/
|
|
87
|
+
userId?: string | number;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Session ID for per-session key derivation (optional).
|
|
91
|
+
* Takes precedence over userId if both provided.
|
|
92
|
+
*/
|
|
93
|
+
sessionId?: string;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Number of PBKDF2 iterations (default: 100000).
|
|
97
|
+
* Must match backend configuration.
|
|
98
|
+
*/
|
|
99
|
+
iterations?: number;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Key prefix for derivation (default: "djangocfg_encryption").
|
|
103
|
+
* Must match backend configuration.
|
|
104
|
+
*/
|
|
105
|
+
keyPrefix?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Result of a decryption operation.
|
|
110
|
+
*/
|
|
111
|
+
export interface DecryptionResult<T = unknown> {
|
|
112
|
+
/** Decrypted data */
|
|
113
|
+
data: T;
|
|
114
|
+
/** Whether decryption was successful */
|
|
115
|
+
success: true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Error from a decryption operation.
|
|
120
|
+
*/
|
|
121
|
+
export interface DecryptionError {
|
|
122
|
+
/** Error message */
|
|
123
|
+
message: string;
|
|
124
|
+
/** Error code */
|
|
125
|
+
code: 'INVALID_FORMAT' | 'DECRYPTION_FAILED' | 'AUTH_FAILED' | 'KEY_ERROR';
|
|
126
|
+
/** Whether decryption was successful */
|
|
127
|
+
success: false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Type guard to check if a value is an encrypted field.
|
|
132
|
+
*/
|
|
133
|
+
export function isEncryptedField(value: unknown): value is EncryptedField {
|
|
134
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
135
|
+
const obj = value as Record<string, unknown>;
|
|
136
|
+
return (
|
|
137
|
+
obj.encrypted === true &&
|
|
138
|
+
typeof obj.algorithm === 'string' &&
|
|
139
|
+
typeof obj.iv === 'string' &&
|
|
140
|
+
typeof obj.data === 'string' &&
|
|
141
|
+
typeof obj.auth_tag === 'string'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Type guard to check if a value is an encrypted response.
|
|
147
|
+
*/
|
|
148
|
+
export function isEncryptedResponse(value: unknown): value is EncryptedResponse {
|
|
149
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
150
|
+
const obj = value as Record<string, unknown>;
|
|
151
|
+
return (
|
|
152
|
+
obj.encrypted === true &&
|
|
153
|
+
typeof obj.algorithm === 'string' &&
|
|
154
|
+
typeof obj.salt === 'string' &&
|
|
155
|
+
typeof obj.iv === 'string' &&
|
|
156
|
+
typeof obj.data === 'string' &&
|
|
157
|
+
typeof obj.auth_tag === 'string'
|
|
158
|
+
);
|
|
159
|
+
}
|