vigor-bridge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +571 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +567 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +21 -0
- package/rollup.config.mjs +25 -0
- package/src/index.ts +672 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
2
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
3
|
+
import typescript from '@rollup/plugin-typescript';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
input: 'src/index.ts',
|
|
7
|
+
output: [
|
|
8
|
+
{
|
|
9
|
+
file: 'dist/index.js',
|
|
10
|
+
format: 'cjs',
|
|
11
|
+
exports: 'named',
|
|
12
|
+
sourcemap: true,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
file: 'dist/index.mjs',
|
|
16
|
+
format: 'esm',
|
|
17
|
+
sourcemap: true,
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
plugins: [
|
|
21
|
+
resolve(),
|
|
22
|
+
commonjs(),
|
|
23
|
+
typescript({ tsconfig: './tsconfig.json' })
|
|
24
|
+
]
|
|
25
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
const Bridge_Error_Messages = {
|
|
2
|
+
INVALID_TYPE: ({expected, received}: {expected: Array<unknown>, received: unknown}) => `Invalid Type: ${typeof received} (expected: ${expected.map(e => JSON.stringify(e)).join(', ')})`,
|
|
3
|
+
INVALID_VALIDATION: ({expected, received}: {expected: Array<string>, received: unknown}) => `Invalid Validation: ${typeof received} (expected: ${expected.join(', ')})`,
|
|
4
|
+
INVALID_PORT: ({expected, received}: {expected: Array<unknown>, received: unknown}) => `Invalid Type: ${typeof received} (expected: ${expected.map(e => JSON.stringify(e)).join(', ')})`,
|
|
5
|
+
MALICIOUS_DATA: ({position, step}: {position: string, step: number}) => `Malicious Data Detected: ${position}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type BridgeErrorCode = keyof typeof Bridge_Error_Messages
|
|
9
|
+
type BridgeErrorData<C extends BridgeErrorCode> =
|
|
10
|
+
Parameters<typeof Bridge_Error_Messages[C]> extends [infer A]
|
|
11
|
+
? A
|
|
12
|
+
: undefined;
|
|
13
|
+
|
|
14
|
+
type BridgeErrorOptions<C extends BridgeErrorCode> = {
|
|
15
|
+
data: BridgeErrorData<C>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class BridgeError<C extends BridgeErrorCode> extends Error {
|
|
19
|
+
public readonly timestamp: Date = new Date()
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
public readonly code: C,
|
|
23
|
+
options: BridgeErrorOptions<C>
|
|
24
|
+
) {
|
|
25
|
+
const messageFn = Bridge_Error_Messages[code] as (args: BridgeErrorData<C>) => string
|
|
26
|
+
const message = `[${code}] ${messageFn(options.data)}`
|
|
27
|
+
super(message)
|
|
28
|
+
this.name = new.target.name
|
|
29
|
+
Object.assign(this, options)
|
|
30
|
+
|
|
31
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
32
|
+
(Error as any).captureStackTrace?.(this, new.target)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class BridgeConnectError<C extends "INVALID_PORT"|"INVALID_VALIDATION"> extends BridgeError<C> {
|
|
37
|
+
constructor(code: C, options: BridgeErrorOptions<C>) {
|
|
38
|
+
super(code, options)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class BridgeDirectError<C extends "MALICIOUS_DATA"> extends BridgeError<C> {
|
|
43
|
+
constructor(code: C, options: BridgeErrorOptions<C>) {
|
|
44
|
+
super(code, options)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class BridgeChannelError<C extends "MALICIOUS_DATA"> extends BridgeError<C> {
|
|
49
|
+
constructor(code: C, options: BridgeErrorOptions<C>) {
|
|
50
|
+
super(code, options)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type BridgeUtilCryptoResult = {
|
|
55
|
+
iv: Array<number>,
|
|
56
|
+
encrypted: Uint8Array<ArrayBuffer>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class BridgeUtilCrypto {
|
|
60
|
+
public static generateRandomHex(length: number = 100) {
|
|
61
|
+
const byteLength = Math.ceil(length / 2);
|
|
62
|
+
const array = new Uint8Array(byteLength);
|
|
63
|
+
|
|
64
|
+
self.crypto.getRandomValues(array);
|
|
65
|
+
|
|
66
|
+
const hex = Array.from(array)
|
|
67
|
+
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
68
|
+
.join('');
|
|
69
|
+
|
|
70
|
+
return hex.substring(0, length);
|
|
71
|
+
}
|
|
72
|
+
public static aes = {
|
|
73
|
+
convertPassword: async(passwordRaw: string) => {
|
|
74
|
+
const encoder = new TextEncoder()
|
|
75
|
+
const password = encoder.encode(passwordRaw.padEnd(32, '0').substring(0, 32))
|
|
76
|
+
|
|
77
|
+
return await window.crypto.subtle.importKey(
|
|
78
|
+
"raw",
|
|
79
|
+
password,
|
|
80
|
+
{ name: "AES-GCM" },
|
|
81
|
+
false,
|
|
82
|
+
["encrypt", "decrypt"]
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
encrypt: async(text: string, passwordRaw: string): Promise<BridgeUtilCryptoResult> => {
|
|
86
|
+
const encoder = new TextEncoder()
|
|
87
|
+
const password = await BridgeUtilCrypto.aes.convertPassword(passwordRaw)
|
|
88
|
+
const data = encoder.encode(text)
|
|
89
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12))
|
|
90
|
+
const encryptedBuffer = await window.crypto.subtle.encrypt(
|
|
91
|
+
{ name: "AES-GCM", iv: iv },
|
|
92
|
+
password,
|
|
93
|
+
data
|
|
94
|
+
)
|
|
95
|
+
return {
|
|
96
|
+
iv: Array.from(iv),
|
|
97
|
+
encrypted: new Uint8Array(encryptedBuffer)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
decrypt: async({iv: ivArray, encrypted}: BridgeUtilCryptoResult, passwordRaw: string) => {
|
|
101
|
+
const decoder = new TextDecoder();
|
|
102
|
+
|
|
103
|
+
const password = await BridgeUtilCrypto.aes.convertPassword(passwordRaw);
|
|
104
|
+
const iv = new Uint8Array(ivArray);
|
|
105
|
+
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
|
106
|
+
{
|
|
107
|
+
name: "AES-GCM",
|
|
108
|
+
iv
|
|
109
|
+
},
|
|
110
|
+
password,
|
|
111
|
+
encrypted
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return decoder.decode(decryptedBuffer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
public static hmac = {
|
|
118
|
+
convertPassword: async (passwordRaw: string): Promise<CryptoKey> => {
|
|
119
|
+
const encoder = new TextEncoder();
|
|
120
|
+
const passwordBytes = encoder.encode(passwordRaw.padEnd(32, '0').substring(0, 32));
|
|
121
|
+
|
|
122
|
+
return await window.crypto.subtle.importKey(
|
|
123
|
+
"raw",
|
|
124
|
+
passwordBytes,
|
|
125
|
+
{
|
|
126
|
+
name: "HMAC",
|
|
127
|
+
hash: { name: "SHA-256" }
|
|
128
|
+
},
|
|
129
|
+
false,
|
|
130
|
+
["sign", "verify"]
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
sign: async(text: string, passwordRaw: string) => {
|
|
134
|
+
const encoder = new TextEncoder()
|
|
135
|
+
const password = await BridgeUtilCrypto.hmac.convertPassword(passwordRaw)
|
|
136
|
+
const data = encoder.encode(text)
|
|
137
|
+
|
|
138
|
+
const encryptedBuffer = await window.crypto.subtle.sign(
|
|
139
|
+
"HMAC",
|
|
140
|
+
password,
|
|
141
|
+
data
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return new Uint8Array(encryptedBuffer) as BufferSource
|
|
145
|
+
},
|
|
146
|
+
verify: async(text: string, passwordRaw: string, signature: BufferSource) => {
|
|
147
|
+
const encoder = new TextEncoder();
|
|
148
|
+
const password = await BridgeUtilCrypto.hmac.convertPassword(passwordRaw)
|
|
149
|
+
const data = encoder.encode(text)
|
|
150
|
+
|
|
151
|
+
return await window.crypto.subtle.verify(
|
|
152
|
+
"HMAC",
|
|
153
|
+
password,
|
|
154
|
+
signature,
|
|
155
|
+
data
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
type BridgeUtilListenerId = symbol & {
|
|
162
|
+
__brand__: "ListenerId"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
class BridgeUtilListener {
|
|
166
|
+
private static listenerMap = new Map<symbol, (event: MessageEvent) => void>()
|
|
167
|
+
public static addEventListener(callback: (event: MessageEvent) => void) {
|
|
168
|
+
const listenerId = Symbol("Listener") as unknown as BridgeUtilListenerId
|
|
169
|
+
window.addEventListener("message", callback)
|
|
170
|
+
this.listenerMap.set(listenerId, callback)
|
|
171
|
+
return {
|
|
172
|
+
id: listenerId,
|
|
173
|
+
removeEventListener: () => this.removeEventListener(listenerId)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
public static removeEventListener(listenerId: BridgeUtilListenerId) {
|
|
177
|
+
const callback = this.listenerMap.get(listenerId)
|
|
178
|
+
if(!callback) return false
|
|
179
|
+
window.removeEventListener("message", callback)
|
|
180
|
+
this.listenerMap.delete(listenerId)
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type BridgeTokensType = {
|
|
186
|
+
validation: {
|
|
187
|
+
request: string
|
|
188
|
+
receive: string
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
type BridgeTokens = {
|
|
193
|
+
validation: string,
|
|
194
|
+
bridge: string,
|
|
195
|
+
testFn: string,
|
|
196
|
+
testResult: string,
|
|
197
|
+
portValidation: {
|
|
198
|
+
host: string,
|
|
199
|
+
remote: string
|
|
200
|
+
},
|
|
201
|
+
directValidation: string
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
type BridgeChannelListenerId = symbol & {
|
|
205
|
+
__brand__: 'Bridge Listener Id'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type BridgeChannelContent = any
|
|
209
|
+
|
|
210
|
+
type BridgeValidationWrapper = (
|
|
211
|
+
getEncrypted: (random: string) => BridgeUtilCryptoResult | Promise<BridgeUtilCryptoResult>
|
|
212
|
+
) => Promise<(<T>(content: BridgeChannelContent) => any)>;
|
|
213
|
+
|
|
214
|
+
class BridgeDirect {
|
|
215
|
+
private caches: Array<{action: string, content: BridgeChannelContent, type: string, host: boolean}> = []
|
|
216
|
+
constructor(
|
|
217
|
+
private listeners: Map<
|
|
218
|
+
BridgeChannelListenerId,
|
|
219
|
+
{ action: string, callback: BridgeValidationWrapper, type: string, host: boolean }
|
|
220
|
+
>,
|
|
221
|
+
private host: boolean,
|
|
222
|
+
private readonly validation: string
|
|
223
|
+
) {
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
public addEventListener(action: string, callback: (content: BridgeChannelContent) => void) {
|
|
227
|
+
const type = "normal"
|
|
228
|
+
for(const cache of this.caches) {
|
|
229
|
+
if(cache.action !== action || cache.type !== type) continue
|
|
230
|
+
callback(cache.content)
|
|
231
|
+
}
|
|
232
|
+
const listenerId = Symbol("LISTENER_NORMAL") as unknown as BridgeChannelListenerId
|
|
233
|
+
this.listeners.set(listenerId, {
|
|
234
|
+
action,
|
|
235
|
+
callback: async(getEncrypted) => {
|
|
236
|
+
const random = BridgeUtilCrypto.generateRandomHex()
|
|
237
|
+
const encryptedRandom = await getEncrypted(random)
|
|
238
|
+
const decryptedRandom = await BridgeUtilCrypto.aes.decrypt(encryptedRandom, this.validation)
|
|
239
|
+
if (decryptedRandom !== random) throw new BridgeDirectError("MALICIOUS_DATA", {
|
|
240
|
+
data: {position: this.host ? "host" : "remote", step: 2}
|
|
241
|
+
})
|
|
242
|
+
return callback
|
|
243
|
+
},
|
|
244
|
+
type,
|
|
245
|
+
host: this.host
|
|
246
|
+
})
|
|
247
|
+
return {
|
|
248
|
+
id: listenerId,
|
|
249
|
+
removeEventListener: () => this.removeEventListener(listenerId)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
public removeEventListener(listenerId: BridgeChannelListenerId) {
|
|
253
|
+
return this.listeners.delete(listenerId)
|
|
254
|
+
}
|
|
255
|
+
public postMessage(action: string, content: BridgeChannelContent) {
|
|
256
|
+
const type = "normal"
|
|
257
|
+
const targets = Array.from(this.listeners.values()).filter(
|
|
258
|
+
listener => listener.action === action && listener.type === type && listener.host !== this.host
|
|
259
|
+
)
|
|
260
|
+
const promises = targets.map(async (listener) => {
|
|
261
|
+
const callback = await listener.callback(async (random: string) => {
|
|
262
|
+
return await BridgeUtilCrypto.aes.encrypt(random, this.validation)
|
|
263
|
+
})
|
|
264
|
+
if (callback) callback(content)
|
|
265
|
+
});
|
|
266
|
+
Promise.all(promises)
|
|
267
|
+
}
|
|
268
|
+
public handle(action: string, callback: <T>(content: BridgeChannelContent) => T|Promise<T>) {
|
|
269
|
+
const type = "request"
|
|
270
|
+
for(const [listenerId, listener] of this.listeners) {
|
|
271
|
+
if(listener.action !== action || listener.type !== type) continue
|
|
272
|
+
this.listeners.delete(listenerId)
|
|
273
|
+
}
|
|
274
|
+
for(const cache of this.caches) {
|
|
275
|
+
if(cache.action !== action || cache.type !== type) continue
|
|
276
|
+
callback(cache.content)
|
|
277
|
+
}
|
|
278
|
+
const listenerId = Symbol("LISTENER_HANDLE") as unknown as BridgeChannelListenerId
|
|
279
|
+
this.listeners.set(listenerId, {
|
|
280
|
+
action,
|
|
281
|
+
callback: async (getEncrypted) => {
|
|
282
|
+
const random = BridgeUtilCrypto.generateRandomHex()
|
|
283
|
+
const encryptedRandom = await getEncrypted(random)
|
|
284
|
+
const decryptedRandom = await BridgeUtilCrypto.aes.decrypt(encryptedRandom, this.validation)
|
|
285
|
+
if (decryptedRandom !== random) throw new BridgeDirectError("MALICIOUS_DATA", {
|
|
286
|
+
data: {position: this.host ? "host" : "remote", step: 2}
|
|
287
|
+
})
|
|
288
|
+
return async(content) => {
|
|
289
|
+
const {requestId, data, host} = content as {requestId: number, data: BridgeChannelContent, host: boolean}
|
|
290
|
+
try {
|
|
291
|
+
const result = await callback(data)
|
|
292
|
+
return {
|
|
293
|
+
action,
|
|
294
|
+
content: {
|
|
295
|
+
success: true,
|
|
296
|
+
data: result,
|
|
297
|
+
error: null,
|
|
298
|
+
responseId: requestId
|
|
299
|
+
},
|
|
300
|
+
type,
|
|
301
|
+
host
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch(error) {
|
|
305
|
+
return {
|
|
306
|
+
action,
|
|
307
|
+
content: {
|
|
308
|
+
success: false,
|
|
309
|
+
data: null,
|
|
310
|
+
error: (error instanceof Error) ? error.message : error,
|
|
311
|
+
responseId: requestId
|
|
312
|
+
},
|
|
313
|
+
type,
|
|
314
|
+
host
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
type,
|
|
320
|
+
host: this.host
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
public request(action: string, content: BridgeChannelContent) {
|
|
324
|
+
return new Promise((resolve, reject) => {
|
|
325
|
+
const requestId = BridgeUtilCrypto.generateRandomHex()
|
|
326
|
+
|
|
327
|
+
const type = "request"
|
|
328
|
+
const listener = Array.from(this.listeners.values()).find(
|
|
329
|
+
listener => listener.action === action && listener.type === type && listener.host !== this.host
|
|
330
|
+
)
|
|
331
|
+
listener?.callback(async (random: string) => {
|
|
332
|
+
const encryptedRandom = await BridgeUtilCrypto.aes.encrypt(random, this.validation)
|
|
333
|
+
return encryptedRandom
|
|
334
|
+
}).then(callback => callback({
|
|
335
|
+
action,
|
|
336
|
+
content: {
|
|
337
|
+
requestId,
|
|
338
|
+
data: content
|
|
339
|
+
},
|
|
340
|
+
type
|
|
341
|
+
}).then(({content}: {content: {success: boolean, data: unknown, error: unknown, responseId: string}}) => {
|
|
342
|
+
const {success, data, error, responseId} = content
|
|
343
|
+
if(responseId !== requestId) return
|
|
344
|
+
|
|
345
|
+
if(success) resolve(data)
|
|
346
|
+
else reject(error)
|
|
347
|
+
}))
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
class BridgeChannel {
|
|
353
|
+
private listeners = new Map<BridgeChannelListenerId, {action: string, callback: (content: BridgeChannelContent) => void, type: string}>()
|
|
354
|
+
private caches: Array<{action: string, content: BridgeChannelContent, type: string}> = []
|
|
355
|
+
constructor(
|
|
356
|
+
private readonly port: MessagePort,
|
|
357
|
+
) {
|
|
358
|
+
port.onmessage = ({data}) => {
|
|
359
|
+
this.caches.push({action: data.action, content: data.content, type: data.type})
|
|
360
|
+
for(const listener of this.listeners.values()) {
|
|
361
|
+
if(listener.action !== data.action || listener.type !== data.type) continue
|
|
362
|
+
listener.callback(data.content)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
public addEventListener(action: string, callback: (content: BridgeChannelContent) => void) {
|
|
367
|
+
const type = "normal"
|
|
368
|
+
for(const cache of this.caches) {
|
|
369
|
+
if(cache.action !== action || cache.type !== type) continue
|
|
370
|
+
callback(cache.content)
|
|
371
|
+
}
|
|
372
|
+
const listenerId = Symbol("LISTENER_NORMAL") as unknown as BridgeChannelListenerId
|
|
373
|
+
this.listeners.set(listenerId, {action, callback, type})
|
|
374
|
+
return {
|
|
375
|
+
id: listenerId,
|
|
376
|
+
removeEventListener: () => this.removeEventListener(listenerId)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
public removeEventListener(listenerId: BridgeChannelListenerId) {
|
|
380
|
+
return this.listeners.delete(listenerId)
|
|
381
|
+
}
|
|
382
|
+
public postMessage(action: string, content: BridgeChannelContent) {
|
|
383
|
+
const type = "normal"
|
|
384
|
+
this.port.postMessage({
|
|
385
|
+
action,
|
|
386
|
+
content,
|
|
387
|
+
type
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
public handle(action: string, callback: <T>(content: BridgeChannelContent) => T|Promise<T>) {
|
|
391
|
+
const type = "request"
|
|
392
|
+
for(const [listenerId, listener] of this.listeners) {
|
|
393
|
+
if(listener.action !== action || listener.type !== type) continue
|
|
394
|
+
this.listeners.delete(listenerId)
|
|
395
|
+
}
|
|
396
|
+
for(const cache of this.caches) {
|
|
397
|
+
if(cache.action !== action || cache.type !== type) continue
|
|
398
|
+
callback(cache.content)
|
|
399
|
+
}
|
|
400
|
+
const listenerId = Symbol("LISTENER_HANDLE") as unknown as BridgeChannelListenerId
|
|
401
|
+
this.listeners.set(listenerId, {action, callback: async(content) => {
|
|
402
|
+
const type = "response"
|
|
403
|
+
const {requestId, data} = content as {requestId: number, data: BridgeChannelContent}
|
|
404
|
+
try {
|
|
405
|
+
const result = await callback(data)
|
|
406
|
+
this.port.postMessage({
|
|
407
|
+
action,
|
|
408
|
+
content: {
|
|
409
|
+
success: true,
|
|
410
|
+
data: result,
|
|
411
|
+
error: null,
|
|
412
|
+
responseId: requestId
|
|
413
|
+
},
|
|
414
|
+
type
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
catch(error) {
|
|
418
|
+
this.port.postMessage({
|
|
419
|
+
action,
|
|
420
|
+
content: {
|
|
421
|
+
success: false,
|
|
422
|
+
data: null,
|
|
423
|
+
error: (error instanceof Error) ? error.message : error,
|
|
424
|
+
responseId: requestId
|
|
425
|
+
},
|
|
426
|
+
type
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
}, type})
|
|
430
|
+
return {
|
|
431
|
+
id: listenerId,
|
|
432
|
+
removeEventListener: () => this.removeEventListener(listenerId)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
public request(action: string, content: BridgeChannelContent) {
|
|
436
|
+
return new Promise((resolve, reject) => {
|
|
437
|
+
const listenerId = Symbol("LISTENER_RESPONSE") as unknown as BridgeChannelListenerId
|
|
438
|
+
const requestId = BridgeUtilCrypto.generateRandomHex()
|
|
439
|
+
this.listeners.set(listenerId, {action, callback: (content) => {
|
|
440
|
+
const {success, data, error, responseId} = content
|
|
441
|
+
if(responseId !== requestId) return
|
|
442
|
+
this.listeners.delete(listenerId)
|
|
443
|
+
|
|
444
|
+
if(success) resolve(data)
|
|
445
|
+
else reject(error)
|
|
446
|
+
}, type: "response"})
|
|
447
|
+
|
|
448
|
+
const type = "request"
|
|
449
|
+
this.port.postMessage({
|
|
450
|
+
action,
|
|
451
|
+
content: {
|
|
452
|
+
requestId,
|
|
453
|
+
data: content
|
|
454
|
+
},
|
|
455
|
+
type
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
public static async create(
|
|
460
|
+
port: MessagePort,
|
|
461
|
+
host: boolean,
|
|
462
|
+
validation: {
|
|
463
|
+
host: string,
|
|
464
|
+
remote: string
|
|
465
|
+
}
|
|
466
|
+
) {
|
|
467
|
+
await new Promise((resolve, reject) => {
|
|
468
|
+
const random = BridgeUtilCrypto.generateRandomHex()
|
|
469
|
+
if(host) {
|
|
470
|
+
port.onmessage = async({data}) => {
|
|
471
|
+
if(data.type === 'port-validation-1') {
|
|
472
|
+
const encryptedRandom = await BridgeUtilCrypto.aes.encrypt(data.content.random, validation.remote)
|
|
473
|
+
port.postMessage({
|
|
474
|
+
type: 'port-validation-2',
|
|
475
|
+
content: {
|
|
476
|
+
random,
|
|
477
|
+
encrypted: encryptedRandom
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
else if(data.type === 'port-validation-3') {
|
|
482
|
+
const decryptedRandom = await BridgeUtilCrypto.aes.decrypt(data.content.encrypted, validation.host)
|
|
483
|
+
if(decryptedRandom !== random) {
|
|
484
|
+
port.close()
|
|
485
|
+
return reject(new BridgeChannelError("MALICIOUS_DATA", {
|
|
486
|
+
data: {
|
|
487
|
+
position: "host",
|
|
488
|
+
step: 3
|
|
489
|
+
}
|
|
490
|
+
}))
|
|
491
|
+
}
|
|
492
|
+
port.postMessage({
|
|
493
|
+
type: 'port-validation-4',
|
|
494
|
+
content: {
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
resolve({})
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
port.close()
|
|
501
|
+
return reject(new BridgeChannelError("MALICIOUS_DATA", {
|
|
502
|
+
data: {
|
|
503
|
+
position: "host",
|
|
504
|
+
step: 0
|
|
505
|
+
}
|
|
506
|
+
}))
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
const portValidationRequest = setInterval(() => {
|
|
512
|
+
port.postMessage({
|
|
513
|
+
type: 'port-validation-1',
|
|
514
|
+
content: {
|
|
515
|
+
random
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
}, 100)
|
|
519
|
+
port.onmessage = async({data}) => {
|
|
520
|
+
clearInterval(portValidationRequest)
|
|
521
|
+
if(data.type === 'port-validation-2') {
|
|
522
|
+
const decryptedRandom = await BridgeUtilCrypto.aes.decrypt(data.content.encrypted, validation.remote)
|
|
523
|
+
if(decryptedRandom !== random) {
|
|
524
|
+
port.close()
|
|
525
|
+
return reject(new BridgeChannelError("MALICIOUS_DATA", {
|
|
526
|
+
data: {
|
|
527
|
+
position: "remote",
|
|
528
|
+
step: 2
|
|
529
|
+
}
|
|
530
|
+
}))
|
|
531
|
+
}
|
|
532
|
+
const encryptedRandom = await BridgeUtilCrypto.aes.encrypt(data.content.random, validation.host)
|
|
533
|
+
port.postMessage({
|
|
534
|
+
type: 'port-validation-3',
|
|
535
|
+
content: {
|
|
536
|
+
encrypted: encryptedRandom
|
|
537
|
+
}
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
else if(data.type === 'port-validation-4') {
|
|
541
|
+
resolve({})
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
port.close()
|
|
545
|
+
return reject(new BridgeChannelError("MALICIOUS_DATA", {
|
|
546
|
+
data: {
|
|
547
|
+
position: "host",
|
|
548
|
+
step: 0
|
|
549
|
+
}
|
|
550
|
+
}))
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
return new BridgeChannel(
|
|
556
|
+
port
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const BridgeEntry = {
|
|
562
|
+
create: () => {
|
|
563
|
+
const type: BridgeTokensType = {
|
|
564
|
+
validation: {
|
|
565
|
+
request: BridgeUtilCrypto.generateRandomHex(),
|
|
566
|
+
receive: BridgeUtilCrypto.generateRandomHex()
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const tokens: BridgeTokens = {
|
|
570
|
+
validation: BridgeUtilCrypto.generateRandomHex(),
|
|
571
|
+
bridge: BridgeUtilCrypto.generateRandomHex(),
|
|
572
|
+
testFn: BridgeUtilCrypto.generateRandomHex(),
|
|
573
|
+
testResult: BridgeUtilCrypto.generateRandomHex(),
|
|
574
|
+
portValidation: {
|
|
575
|
+
host: BridgeUtilCrypto.generateRandomHex(),
|
|
576
|
+
remote: BridgeUtilCrypto.generateRandomHex()
|
|
577
|
+
},
|
|
578
|
+
directValidation: BridgeUtilCrypto.generateRandomHex()
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const bridge = new Promise<BridgeChannel|BridgeDirect>(async(resolve, reject) => {
|
|
582
|
+
const connectReceive = BridgeUtilListener.addEventListener(async(event) => {
|
|
583
|
+
const {data} = event
|
|
584
|
+
if(data.type !== type.validation.request) return
|
|
585
|
+
connectReceive.removeEventListener()
|
|
586
|
+
const main = (window as any)[tokens.bridge]
|
|
587
|
+
delete (window as any)[tokens.bridge]
|
|
588
|
+
const shareContext = ((main) => {
|
|
589
|
+
if(main instanceof Map) {
|
|
590
|
+
const testFn = main.get(tokens.testFn)
|
|
591
|
+
if(typeof testFn === 'function' && testFn() === tokens.testResult) return true
|
|
592
|
+
}
|
|
593
|
+
return false
|
|
594
|
+
})(main)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
const encryptedRandom = await BridgeUtilCrypto.aes.encrypt(data.content.random, tokens.validation)
|
|
598
|
+
|
|
599
|
+
if(shareContext) {
|
|
600
|
+
window.postMessage({
|
|
601
|
+
type: type.validation.receive,
|
|
602
|
+
content: {
|
|
603
|
+
encrypted: encryptedRandom,
|
|
604
|
+
shareContext
|
|
605
|
+
}
|
|
606
|
+
}, event.origin)
|
|
607
|
+
resolve(new BridgeDirect(main, true, tokens.directValidation))
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
const channel = new MessageChannel()
|
|
611
|
+
const port1 = channel.port1
|
|
612
|
+
window.postMessage({
|
|
613
|
+
type: type.validation.receive,
|
|
614
|
+
content: {
|
|
615
|
+
encrypted: encryptedRandom,
|
|
616
|
+
shareContext
|
|
617
|
+
}
|
|
618
|
+
}, event.origin, [channel.port2])
|
|
619
|
+
|
|
620
|
+
resolve(await BridgeChannel.create(port1, true, tokens.portValidation))
|
|
621
|
+
}
|
|
622
|
+
})
|
|
623
|
+
})
|
|
624
|
+
return {certification: {tokens, type}, bridge}
|
|
625
|
+
},
|
|
626
|
+
connect: ({tokens, type}: {tokens: BridgeTokens, type: BridgeTokensType}) => {
|
|
627
|
+
return new Promise<BridgeChannel|BridgeDirect>(async(resolve, reject) => {
|
|
628
|
+
const random = BridgeUtilCrypto.generateRandomHex()
|
|
629
|
+
const main = (window as any)[tokens.bridge] = new Map([[tokens.testFn, () => tokens.testResult]]) as Map<any,any>
|
|
630
|
+
const connectValidationRequest = setInterval(() => {
|
|
631
|
+
window.postMessage({
|
|
632
|
+
type: type.validation.request,
|
|
633
|
+
content: {
|
|
634
|
+
random
|
|
635
|
+
}
|
|
636
|
+
})
|
|
637
|
+
}, 100)
|
|
638
|
+
const connectValidationReceive = BridgeUtilListener.addEventListener(async(event) => {
|
|
639
|
+
const {data} = event
|
|
640
|
+
if(data.type !== type.validation.receive) return
|
|
641
|
+
connectValidationReceive.removeEventListener()
|
|
642
|
+
clearInterval(connectValidationRequest)
|
|
643
|
+
|
|
644
|
+
const decryptedRandom = await BridgeUtilCrypto.aes.decrypt(data.content.encrypted, tokens.validation)
|
|
645
|
+
if(random !== decryptedRandom) return reject(new BridgeConnectError("INVALID_VALIDATION", {
|
|
646
|
+
data: {
|
|
647
|
+
expected: [random],
|
|
648
|
+
received: decryptedRandom
|
|
649
|
+
}
|
|
650
|
+
}))
|
|
651
|
+
const shareContext = data.content.shareContext
|
|
652
|
+
|
|
653
|
+
if(shareContext) {
|
|
654
|
+
resolve(new BridgeDirect(main, false, tokens.directValidation))
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
const [port2] = event.ports
|
|
658
|
+
if(!(port2 instanceof MessagePort)) return reject(new BridgeConnectError("INVALID_PORT", {
|
|
659
|
+
data: {
|
|
660
|
+
expected: ["MessagePort"],
|
|
661
|
+
received: port2
|
|
662
|
+
}
|
|
663
|
+
}))
|
|
664
|
+
|
|
665
|
+
resolve(await BridgeChannel.create(port2, false, tokens.portValidation))
|
|
666
|
+
}
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export default BridgeEntry
|