partner_react_native_sdk 0.1.8 → 1.0.1
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/lib/module/helpers/network/APICall.js +75 -96
- package/lib/module/helpers/network/APICall.js.map +1 -1
- package/lib/module/helpers/network/Encryption.js +146 -239
- package/lib/module/helpers/network/Encryption.js.map +1 -1
- package/lib/module/helpers/partner_library_react_native.js +81 -0
- package/lib/module/helpers/partner_library_react_native.js.map +1 -1
- package/lib/module/helpers/webview.js +70 -0
- package/lib/module/helpers/webview.js.map +1 -1
- package/lib/typescript/src/helpers/network/APICall.d.ts +1 -0
- package/lib/typescript/src/helpers/network/APICall.d.ts.map +1 -1
- package/lib/typescript/src/helpers/network/Encryption.d.ts +23 -0
- package/lib/typescript/src/helpers/network/Encryption.d.ts.map +1 -1
- package/lib/typescript/src/helpers/partner_library_react_native.d.ts +9 -0
- package/lib/typescript/src/helpers/partner_library_react_native.d.ts.map +1 -1
- package/lib/typescript/src/helpers/webview.d.ts +10 -0
- package/lib/typescript/src/helpers/webview.d.ts.map +1 -1
- package/package.json +9 -6
- package/src/helpers/network/APICall.tsx +99 -94
- package/src/helpers/network/Encryption.tsx +179 -239
- package/src/helpers/partner_library_react_native.tsx +106 -0
- package/src/helpers/webview.tsx +87 -0
- package/lib/typescript/rn78test/App.d.ts +0 -10
- package/lib/typescript/rn78test/App.d.ts.map +0 -1
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import NetworkManager from './network_manager';
|
|
3
3
|
import { HeaderManager } from '../utils/headerManager';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
import {
|
|
6
|
+
generateAESKey,
|
|
7
|
+
exportAESKeyBase64,
|
|
8
|
+
encryptAESKeyWithRSA,
|
|
9
|
+
encryptAES,
|
|
10
|
+
decryptAES,
|
|
11
|
+
getWebsiteKey,
|
|
12
|
+
} from './Encryption';
|
|
13
13
|
|
|
14
14
|
export class APICall {
|
|
15
15
|
private networkManager: NetworkManager = new NetworkManager();
|
|
@@ -24,7 +24,7 @@ export class APICall {
|
|
|
24
24
|
encrypted?: boolean;
|
|
25
25
|
}
|
|
26
26
|
): Promise<any> {
|
|
27
|
-
const { body, headers, encrypted =
|
|
27
|
+
const { body, headers, encrypted = true } = options || {};
|
|
28
28
|
try {
|
|
29
29
|
const response = encrypted
|
|
30
30
|
? await this._callAPIEncrypt(method, apiUrl, body, headers)
|
|
@@ -55,96 +55,101 @@ export class APICall {
|
|
|
55
55
|
headers: mergedHeaders,
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
// ----------------- ENCRYPTED CALL -----------------
|
|
58
|
+
// ----------------- ENCRYPTED CALL (quick-crypto) -----------------
|
|
60
59
|
private async _callAPIEncrypt(
|
|
61
60
|
method: string,
|
|
62
61
|
apiUrl: string,
|
|
63
62
|
body?: Record<string, any>,
|
|
64
63
|
headers?: Record<string, any>
|
|
65
64
|
): Promise<any> {
|
|
66
|
-
// 1) Generate ephemeral AES-256 key
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
65
|
+
// 1) Generate ephemeral AES-256 key (Uint8Array)
|
|
66
|
+
const aesKey = generateAESKey(); // Uint8Array (32 bytes)
|
|
67
|
+
|
|
68
|
+
// 2) Export AES key bytes -> Base64 (for RSA input)
|
|
69
|
+
const aesKeyBase64 = exportAESKeyBase64(aesKey); // string
|
|
70
|
+
|
|
71
|
+
// 3) Fetch server public key (cached) and RSA-wrap the AES key
|
|
72
|
+
const websiteKey = await getWebsiteKey(); // { kid, public, expiry }
|
|
73
|
+
const rsaEncryptedKey = encryptAESKeyWithRSA(
|
|
74
|
+
aesKeyBase64,
|
|
75
|
+
websiteKey.public
|
|
76
|
+
); // string
|
|
77
|
+
|
|
78
|
+
// 4) Encrypt payload with AES-GCM -> Base64(IV||CT||TAG)
|
|
79
|
+
const encryptedPayload = encryptAES(JSON.stringify(body ?? {}), aesKey); // string
|
|
80
|
+
|
|
81
|
+
// 5) Merge headers (stringify all values)
|
|
82
|
+
const baseHeaders = await this.headerManager.generateHeaders();
|
|
83
|
+
const mergedHeaders: Record<string, string> = {
|
|
84
|
+
...Object.fromEntries(
|
|
85
|
+
Object.entries(baseHeaders).map(([k, v]) => [k, String(v)])
|
|
86
|
+
),
|
|
87
|
+
...(headers
|
|
88
|
+
? Object.fromEntries(
|
|
89
|
+
Object.entries(headers).map(([k, v]) => [k, String(v)])
|
|
90
|
+
)
|
|
91
|
+
: {}),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// 6) Add encryption headers
|
|
95
|
+
const encryptedHeaders: Record<string, string> = {
|
|
96
|
+
'Content-Type': 'application/json;charset=utf-8',
|
|
97
|
+
'Accept': 'application/json',
|
|
98
|
+
...mergedHeaders,
|
|
99
|
+
'encryption_kid': websiteKey.kid ?? '',
|
|
100
|
+
'key': rsaEncryptedKey ?? '',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// 7) Make the encrypted request with { encrypted: "<blob>" }
|
|
104
|
+
const response = await this.networkManager.request({
|
|
105
|
+
url: apiUrl,
|
|
106
|
+
method,
|
|
107
|
+
body: { encrypted: encryptedPayload },
|
|
108
|
+
headers: encryptedHeaders,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 8) Decrypt response if it has { encrypted }
|
|
112
|
+
const decrypted = APICall.handleDecryptedResponse(response, aesKey);
|
|
113
|
+
if (decrypted != null) {
|
|
114
|
+
response!.data = decrypted;
|
|
115
|
+
}
|
|
116
|
+
return response;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ----------------- RESPONSE DECRYPT (quick-crypto) -----------------
|
|
120
|
+
static handleDecryptedResponse(
|
|
121
|
+
response: any,
|
|
122
|
+
aesKey: Uint8Array
|
|
123
|
+
): Record<string, any> | null {
|
|
124
|
+
try {
|
|
125
|
+
if (!response) return null;
|
|
126
|
+
|
|
127
|
+
const raw = response.data;
|
|
128
|
+
// Accept string or object
|
|
129
|
+
const data: Record<string, any> =
|
|
130
|
+
typeof raw === 'string' ? JSON.parse(raw) : (raw as any);
|
|
131
|
+
|
|
132
|
+
if (!data || typeof data !== 'object' || !('encrypted' in data)) {
|
|
133
|
+
// no encrypted field, return null to signal "not decrypted"
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const encryptedPayload = String(data.encrypted ?? '');
|
|
138
|
+
if (!encryptedPayload) return null;
|
|
139
|
+
|
|
140
|
+
const decryptedJson = decryptAES(encryptedPayload, aesKey); // sync
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const obj = JSON.parse(decryptedJson);
|
|
144
|
+
return obj && typeof obj === 'object'
|
|
145
|
+
? (obj as Record<string, any>)
|
|
146
|
+
: null;
|
|
147
|
+
} catch {
|
|
148
|
+
// decrypted payload wasn't valid JSON
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
149
154
|
}
|
|
150
155
|
}
|
|
@@ -1,239 +1,179 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// if (!values.length || typeof values[0] !== 'object') {
|
|
181
|
-
// throw new Error('Unexpected keyset response shape');
|
|
182
|
-
// }
|
|
183
|
-
// obj = values[0];
|
|
184
|
-
// } else {
|
|
185
|
-
// obj = raw;
|
|
186
|
-
// }
|
|
187
|
-
|
|
188
|
-
// const kid: string | undefined = obj.kid ?? obj.KID;
|
|
189
|
-
// const publicKey: string | undefined = obj.public ?? obj.public_key;
|
|
190
|
-
// const expiry: string | undefined = obj.expiry ?? obj.exp ?? obj.expiresAt;
|
|
191
|
-
|
|
192
|
-
// if (!kid || !publicKey || !expiry) {
|
|
193
|
-
// throw new Error('Missing kid/public/expiry in website key response');
|
|
194
|
-
// }
|
|
195
|
-
|
|
196
|
-
// const normalized: WebsiteKey = { kid, public: publicKey, expiry };
|
|
197
|
-
|
|
198
|
-
// await AsyncStorage.setItem(KEY_WEB, JSON.stringify(normalized));
|
|
199
|
-
// await AsyncStorage.setItem(KEY_WEB_EXPIRY, expiry);
|
|
200
|
-
|
|
201
|
-
// return normalized;
|
|
202
|
-
// }
|
|
203
|
-
|
|
204
|
-
// // ---------- Optional: Hybrid helpers (one-liners) ----------
|
|
205
|
-
|
|
206
|
-
// export type HybridEnvelope = {
|
|
207
|
-
// kid: string;
|
|
208
|
-
// rsaWrappedAESKey: string; // Base64 RSA(OAEP) of Base64(AES key)
|
|
209
|
-
// cipherData: string; // Base64( IV || ciphertext || tag )
|
|
210
|
-
// };
|
|
211
|
-
|
|
212
|
-
// export async function encryptHybrid(
|
|
213
|
-
// plaintext: string
|
|
214
|
-
// ): Promise<HybridEnvelope> {
|
|
215
|
-
// // 1) fetch server key
|
|
216
|
-
// const { kid, public: publicPem } = await getWebsiteKey();
|
|
217
|
-
|
|
218
|
-
// // 2) generate AES key & encrypt data
|
|
219
|
-
// const aesKey = await generateAESKey();
|
|
220
|
-
// const cipherData = await encryptAES(plaintext, aesKey);
|
|
221
|
-
|
|
222
|
-
// // 3) export AES key to Base64 and RSA-wrap it
|
|
223
|
-
// const aesKeyB64 = await exportAESKeyBase64(aesKey);
|
|
224
|
-
// const rsaWrappedAESKey = await encryptAESKeyWithRSA(aesKeyB64, publicPem);
|
|
225
|
-
|
|
226
|
-
// return { kid, rsaWrappedAESKey, cipherData };
|
|
227
|
-
// }
|
|
228
|
-
|
|
229
|
-
// /**
|
|
230
|
-
// * Decrypt using an already-imported AES key (symmetric path).
|
|
231
|
-
// * Use this for local decrypts (e.g., testing) or when the server sends you encrypted data
|
|
232
|
-
// * and you already have the AES key.
|
|
233
|
-
// */
|
|
234
|
-
// export async function decryptHybridLocal(
|
|
235
|
-
// cipherData: string,
|
|
236
|
-
// aesKey: CryptoKey
|
|
237
|
-
// ): Promise<string> {
|
|
238
|
-
// return await decryptAES(cipherData, aesKey);
|
|
239
|
-
// }
|
|
1
|
+
// Encryption.ts
|
|
2
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import * as base64js from 'base64-js';
|
|
5
|
+
import { ServiceNames } from '../ServiceNames';
|
|
6
|
+
|
|
7
|
+
import crypto from 'react-native-quick-crypto';
|
|
8
|
+
import { Buffer } from '@craftzdog/react-native-buffer';
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
randomBytes,
|
|
12
|
+
createCipheriv,
|
|
13
|
+
createDecipheriv,
|
|
14
|
+
publicEncrypt,
|
|
15
|
+
constants,
|
|
16
|
+
createPublicKey,
|
|
17
|
+
} = crypto;
|
|
18
|
+
|
|
19
|
+
type WebsiteKey = { kid: string; public: string; expiry: string };
|
|
20
|
+
|
|
21
|
+
const KEY_WEB = 'KEY_WEB';
|
|
22
|
+
const KEY_WEB_EXPIRY = 'KEY_WEB_EXPIRY';
|
|
23
|
+
const GCM_NONCE_LEN = 12; // 12-byte IV
|
|
24
|
+
const GCM_TAG_LEN = 16; // 16-byte tag
|
|
25
|
+
const AES_KEY_LEN_BYTES = 32; // 256 bits
|
|
26
|
+
|
|
27
|
+
// --- small utils ---
|
|
28
|
+
const bytesToB64 = (u8: Uint8Array) => base64js.fromByteArray(u8);
|
|
29
|
+
const b64ToBytes = (b64: string) => base64js.toByteArray(b64);
|
|
30
|
+
|
|
31
|
+
// ---------- AES-256-GCM ----------
|
|
32
|
+
|
|
33
|
+
/** Generate random 32-byte AES key */
|
|
34
|
+
export function generateAESKey(): Uint8Array {
|
|
35
|
+
return new Uint8Array(randomBytes(AES_KEY_LEN_BYTES));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Export AES key bytes -> Base64 (for RSA input) */
|
|
39
|
+
export function exportAESKeyBase64(keyBytes: Uint8Array): string {
|
|
40
|
+
return bytesToB64(keyBytes);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Encrypt plaintext -> Base64( IV || ciphertext || tag ) */
|
|
44
|
+
export function encryptAES(plaintext: string, keyBytes: Uint8Array): string {
|
|
45
|
+
const iv = new Uint8Array(randomBytes(GCM_NONCE_LEN));
|
|
46
|
+
const cipher = createCipheriv(
|
|
47
|
+
'aes-256-gcm',
|
|
48
|
+
Buffer.from(keyBytes),
|
|
49
|
+
Buffer.from(iv)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const ct1 = cipher.update(plaintext, 'utf8');
|
|
53
|
+
const ct2 = cipher.final();
|
|
54
|
+
const ciphertext = Buffer.concat([ct1, ct2]);
|
|
55
|
+
|
|
56
|
+
const tag = cipher.getAuthTag(); // 16 bytes
|
|
57
|
+
|
|
58
|
+
const out = new Uint8Array(iv.length + ciphertext.length + tag.length);
|
|
59
|
+
out.set(iv, 0);
|
|
60
|
+
out.set(ciphertext, iv.length);
|
|
61
|
+
out.set(tag, iv.length + ciphertext.length);
|
|
62
|
+
|
|
63
|
+
return bytesToB64(out);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Decrypt Base64( IV || ciphertext || tag ) -> plaintext */
|
|
67
|
+
export function decryptAES(b64Data: string, keyBytes: Uint8Array): string {
|
|
68
|
+
const data = b64ToBytes(b64Data);
|
|
69
|
+
if (data.length < GCM_NONCE_LEN + GCM_TAG_LEN + 1) {
|
|
70
|
+
throw new Error('Invalid GCM payload length');
|
|
71
|
+
}
|
|
72
|
+
const iv = data.slice(0, GCM_NONCE_LEN);
|
|
73
|
+
const tag = data.slice(data.length - GCM_TAG_LEN);
|
|
74
|
+
const ct = data.slice(GCM_NONCE_LEN, data.length - GCM_TAG_LEN);
|
|
75
|
+
|
|
76
|
+
const decipher = createDecipheriv(
|
|
77
|
+
'aes-256-gcm',
|
|
78
|
+
Buffer.from(keyBytes),
|
|
79
|
+
Buffer.from(iv)
|
|
80
|
+
);
|
|
81
|
+
decipher.setAuthTag(Buffer.from(tag));
|
|
82
|
+
|
|
83
|
+
const pt1 = decipher.update(Buffer.from(ct));
|
|
84
|
+
const pt2 = decipher.final();
|
|
85
|
+
return Buffer.concat([pt1, pt2]).toString('utf8');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------- RSA-OAEP (SHA-256) ----------
|
|
89
|
+
|
|
90
|
+
/** RSA-OAEP(SHA-256) encrypt UTF-8 bytes of Base64(AES key) -> Base64 ciphertext */
|
|
91
|
+
export function encryptAESKeyWithRSA(
|
|
92
|
+
aesKeyBase64: string,
|
|
93
|
+
publicKeyPem: string
|
|
94
|
+
): string {
|
|
95
|
+
const buf = Buffer.from(aesKeyBase64, 'utf8');
|
|
96
|
+
const enc = publicEncrypt(
|
|
97
|
+
{
|
|
98
|
+
key: publicKeyPem,
|
|
99
|
+
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
|
100
|
+
oaepHash: 'sha256',
|
|
101
|
+
},
|
|
102
|
+
buf
|
|
103
|
+
);
|
|
104
|
+
return enc.toString('base64');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------- Website key fetch / cache ----------
|
|
108
|
+
|
|
109
|
+
export async function getWebsiteKey(): Promise<WebsiteKey> {
|
|
110
|
+
// const cachedJson = await AsyncStorage.getItem(KEY_WEB);
|
|
111
|
+
// const cachedExpiry = await AsyncStorage.getItem(KEY_WEB_EXPIRY);
|
|
112
|
+
// if (cachedJson && cachedExpiry) {
|
|
113
|
+
// try {
|
|
114
|
+
// if (new Date() < new Date(cachedExpiry)) {
|
|
115
|
+
// return JSON.parse(cachedJson) as WebsiteKey;
|
|
116
|
+
// }
|
|
117
|
+
// } catch {
|
|
118
|
+
// /* ignore */
|
|
119
|
+
// }
|
|
120
|
+
// }
|
|
121
|
+
|
|
122
|
+
// const txnId = uuidv4();
|
|
123
|
+
const resp = await fetch(ServiceNames.WEBSITE_KEYS, {
|
|
124
|
+
method: 'GET',
|
|
125
|
+
// headers: { 'X-Txn-ID': txnId },
|
|
126
|
+
});
|
|
127
|
+
if (!resp.ok) throw new Error(`Website keys fetch failed: ${resp.status}`);
|
|
128
|
+
|
|
129
|
+
const raw = await resp.json();
|
|
130
|
+
console.log('Website keys response:', raw);
|
|
131
|
+
let obj: any;
|
|
132
|
+
if (
|
|
133
|
+
raw &&
|
|
134
|
+
typeof raw === 'object' &&
|
|
135
|
+
!(
|
|
136
|
+
Object.prototype.hasOwnProperty.call(raw, 'kid') &&
|
|
137
|
+
(Object.prototype.hasOwnProperty.call(raw, 'public') ||
|
|
138
|
+
Object.prototype.hasOwnProperty.call(raw, 'public_key'))
|
|
139
|
+
)
|
|
140
|
+
) {
|
|
141
|
+
const values = Object.values(raw);
|
|
142
|
+
if (!values.length || typeof values[0] !== 'object') {
|
|
143
|
+
throw new Error('Unexpected keyset response shape');
|
|
144
|
+
}
|
|
145
|
+
obj = values[0];
|
|
146
|
+
} else {
|
|
147
|
+
obj = raw;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const kid: string | undefined = obj.kid ?? obj.KID;
|
|
151
|
+
const publicKey: string | undefined = obj.public ?? obj.public_key;
|
|
152
|
+
const expiry: string | undefined = obj.expiry ?? obj.exp ?? obj.expiresAt;
|
|
153
|
+
if (!kid || !publicKey || !expiry)
|
|
154
|
+
throw new Error('Missing kid/public/expiry');
|
|
155
|
+
|
|
156
|
+
const normalized: WebsiteKey = { kid, public: publicKey, expiry };
|
|
157
|
+
await AsyncStorage.setItem(KEY_WEB, JSON.stringify(normalized));
|
|
158
|
+
await AsyncStorage.setItem(KEY_WEB_EXPIRY, expiry);
|
|
159
|
+
return normalized;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------- Optional: one-liners ----------
|
|
163
|
+
|
|
164
|
+
export type HybridEnvelope = {
|
|
165
|
+
kid: string;
|
|
166
|
+
rsaWrappedAESKey: string; // Base64 RSA(OAEP) of Base64(AES key)
|
|
167
|
+
cipherData: string; // Base64( IV || ciphertext || tag )
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export async function encryptHybrid(
|
|
171
|
+
plaintext: string
|
|
172
|
+
): Promise<HybridEnvelope> {
|
|
173
|
+
const { kid, public: publicPem } = await getWebsiteKey();
|
|
174
|
+
const aesKey = generateAESKey();
|
|
175
|
+
const cipherData = encryptAES(plaintext, aesKey);
|
|
176
|
+
const aesKeyB64 = exportAESKeyBase64(aesKey);
|
|
177
|
+
const rsaWrappedAESKey = encryptAESKeyWithRSA(aesKeyB64, publicPem);
|
|
178
|
+
return { kid, rsaWrappedAESKey, cipherData };
|
|
179
|
+
}
|