@tdxvolt/volt-client-grpc 0.16.0-beta.9 → 0.18.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/README.md +39 -3
- package/lib/index.cjs +458 -515
- package/package.json +2 -4
- package/protobuf/tdx/volt_api/data/v1/sqlite_database_api.proto +49 -13
- package/protobuf/tdx/volt_api/data/v1/sqlite_server_api.proto +3 -0
- package/protobuf/tdx/volt_api/volt/v1/discovery_api.proto +0 -4
- package/protobuf/tdx/volt_api/volt/v1/ssi.proto +31 -5
- package/protobuf/tdx/volt_api/volt/v1/ssi_api.proto +121 -33
- package/protobuf/tdx/volt_api/volt/v1/terminal_api.proto +42 -0
- package/protobuf/tdx/volt_api/volt/v1/volt.proto +152 -140
- package/protobuf/tdx/volt_api/volt/v1/volt_api.proto +168 -270
- package/protobuf/tdx/volt_api/volt/v1/wire_api.proto +4 -0
- package/src/constants.js +4 -58
- package/src/grpc-call.js +144 -51
- package/src/grpc-utils.js +8 -4
- package/src/rpc-invocation.js +96 -46
- package/src/utils.js +5 -119
- package/src/volt-client-internal.js +109 -78
- package/src/volt-client.js +33 -12
- package/src/volt-connection.js +25 -16
- package/src/volt-credential.js +39 -124
|
@@ -22,6 +22,10 @@ message PublishWireRequest {
|
|
|
22
22
|
|
|
23
23
|
// The chunk of data to publish.
|
|
24
24
|
bytes chunk = 2;
|
|
25
|
+
|
|
26
|
+
// Whether to persist the chunk.
|
|
27
|
+
// This is only valid if the wire is configured to persist messages, i.e. the `volt:wire-persist` attribute is true.
|
|
28
|
+
bool do_not_persist = 3;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
message PublishWireResponse {
|
package/src/constants.js
CHANGED
|
@@ -1,59 +1,10 @@
|
|
|
1
|
-
// eslint-disable-next-line import/prefer-default-export
|
|
2
1
|
export const constants = {
|
|
3
|
-
authTokenName: "
|
|
4
|
-
|
|
5
|
-
add: "ADD",
|
|
6
|
-
remove: "REMOVE",
|
|
7
|
-
update: "UPDATE"
|
|
8
|
-
},
|
|
9
|
-
crypto: {
|
|
10
|
-
subjectDefaults: [
|
|
11
|
-
{
|
|
12
|
-
name: "countryName",
|
|
13
|
-
value: "GB"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
name: "localityName",
|
|
17
|
-
value: "Southampton"
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: "organizationName",
|
|
21
|
-
value: "nquiringminds Ltd"
|
|
22
|
-
}
|
|
23
|
-
],
|
|
24
|
-
userIdOID: "0.9.2342.19200300.100.1.1"
|
|
25
|
-
},
|
|
26
|
-
defaultDIDHostName: "cloud.tdxvolt.com",
|
|
2
|
+
authTokenName: "volt-token",
|
|
3
|
+
defaultDIDHostName: "tdxvolt.com",
|
|
27
4
|
didServiceType: {
|
|
28
|
-
voltConfig: "
|
|
29
|
-
},
|
|
30
|
-
identityType: {
|
|
31
|
-
did: "did",
|
|
32
|
-
volt: "volt"
|
|
33
|
-
},
|
|
34
|
-
onlineStatus: {
|
|
35
|
-
offline: "offline",
|
|
36
|
-
online: "online",
|
|
37
|
-
unknown: "unknown"
|
|
5
|
+
voltConfig: "volt-discovery"
|
|
38
6
|
},
|
|
39
|
-
publicKeyPrefix: "-----BEGIN PUBLIC KEY-----",
|
|
40
|
-
privateKeyPrefix: "-----BEGIN RSA PRIVATE KEY-----",
|
|
41
7
|
privateEncryptedKeyPrefix: "-----BEGIN ENCRYPTED PRIVATE KEY-----",
|
|
42
|
-
resource: {
|
|
43
|
-
identity: "ident",
|
|
44
|
-
identityView: "ident-view",
|
|
45
|
-
proxy: "proxy",
|
|
46
|
-
proxyView: "proxy-view",
|
|
47
|
-
vc: "vc",
|
|
48
|
-
vcView: "vc-view"
|
|
49
|
-
},
|
|
50
|
-
voltStatus: {
|
|
51
|
-
pending: "",
|
|
52
|
-
online: "online",
|
|
53
|
-
offline: "offline",
|
|
54
|
-
deleted: "deleted",
|
|
55
|
-
revoked: "revoked"
|
|
56
|
-
},
|
|
57
8
|
/**
|
|
58
9
|
* These top-level service types should map to a protobuf definition
|
|
59
10
|
* in @tdxvolt/volt-proto.
|
|
@@ -66,10 +17,5 @@ export const constants = {
|
|
|
66
17
|
relayAPI: "tdx.volt_api.relay.v1.RelayAPI",
|
|
67
18
|
voltAPI: "tdx.volt_api.volt.v1.VoltAPI",
|
|
68
19
|
wireAPI: "tdx.volt_api.volt.v1.WireAPI"
|
|
69
|
-
}
|
|
70
|
-
tunnelInvokeMethod: "/tdx.volt_api.volt.v1.VoltAPI/Invoke",
|
|
71
|
-
tunnelPingInterval: 10000,
|
|
72
|
-
tunnelPingTimeout: 2500,
|
|
73
|
-
tunnelReconnectAfter: 30000,
|
|
74
|
-
tunnelTimeoutInterval: 10000 * 2 // This must always be greater than `tunnelPingInterval`.
|
|
20
|
+
}
|
|
75
21
|
};
|
package/src/grpc-call.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
/* eslint-disable no-underscore-dangle */
|
|
2
2
|
import debug from "debug";
|
|
3
3
|
import EventEmitter from "events";
|
|
4
|
-
import {
|
|
4
|
+
import { voltUtils as voltClientUtils } from "@tdxvolt/volt-client-web/js";
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
aesCreateKey,
|
|
8
|
+
aesEncrypt,
|
|
9
|
+
aesDecrypt,
|
|
10
|
+
toBase64,
|
|
11
|
+
formatPEM,
|
|
12
|
+
keyFromPem,
|
|
13
|
+
verify,
|
|
14
|
+
deriveSharedKey,
|
|
15
|
+
x25519
|
|
16
|
+
} = voltClientUtils;
|
|
5
17
|
|
|
6
18
|
const log = debug("volt-client-grpc:grpc-call");
|
|
7
19
|
|
|
@@ -14,6 +26,21 @@ function _prepareInvokeRequest(
|
|
|
14
26
|
this._service?.service_description.host_type ===
|
|
15
27
|
"SERVICE_HOST_TYPE_RELAYED";
|
|
16
28
|
|
|
29
|
+
const relaying = this._voltClient.isVoltRelay || isServiceRelayed;
|
|
30
|
+
|
|
31
|
+
let targetDID = [];
|
|
32
|
+
if (this._voltClient.isVoltRelay) {
|
|
33
|
+
targetDID.push(this._voltClient.voltConfig.id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isServiceRelayed) {
|
|
37
|
+
// Add another relay hop if the target service is relayed.
|
|
38
|
+
log("target service is relayed");
|
|
39
|
+
targetDID.push(this._service.service_description.host_client_id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log("target is %j", targetDID);
|
|
43
|
+
|
|
17
44
|
// Use the service's host client id and public key if the service is relayed.
|
|
18
45
|
const methodToken = this._voltClient.credential.getIdentityMetadata(
|
|
19
46
|
this._voltClient.grpc,
|
|
@@ -21,17 +48,52 @@ function _prepareInvokeRequest(
|
|
|
21
48
|
this._voltClient.voltConfig.id,
|
|
22
49
|
this._service?.service_description.host_public_key ||
|
|
23
50
|
this._voltClient.credential.voltPublicKey,
|
|
24
|
-
|
|
51
|
+
relaying
|
|
25
52
|
);
|
|
26
53
|
|
|
27
54
|
let invokeRequest;
|
|
28
55
|
let callMethod = method;
|
|
29
56
|
|
|
30
|
-
if (
|
|
57
|
+
if (relaying) {
|
|
31
58
|
//
|
|
32
59
|
// If the volt connection is via a relay (or the target service is relayed),
|
|
33
60
|
// we wrap the rpc in a RemoteRequest message and send it via a call to Invoke().
|
|
34
61
|
//
|
|
62
|
+
// Replace the target method with a call to Invoke on the relay Volt.
|
|
63
|
+
//
|
|
64
|
+
callMethod = "Invoke";
|
|
65
|
+
|
|
66
|
+
// But first we need to perform a key exchange with the target service.
|
|
67
|
+
//
|
|
68
|
+
if (!this._encryptionKey) {
|
|
69
|
+
// We haven't exchanged keys yet, so queue the payload.
|
|
70
|
+
this._pendingRequests.push(request);
|
|
71
|
+
|
|
72
|
+
if (this._keyExchangePending) {
|
|
73
|
+
// We've already sent the key exchange request, so do nothing.
|
|
74
|
+
log("key exchange pending, not sending payload");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this._keyExchangePending = true;
|
|
79
|
+
|
|
80
|
+
// Send the key exchange request.
|
|
81
|
+
invokeRequest = {
|
|
82
|
+
token: methodToken.token,
|
|
83
|
+
target_did: targetDID,
|
|
84
|
+
target_service_id: isServiceRelayed
|
|
85
|
+
? this._service.service_description.host_service_id
|
|
86
|
+
: undefined
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
request: invokeRequest,
|
|
91
|
+
method: callMethod,
|
|
92
|
+
methodName: method,
|
|
93
|
+
meta: methodToken
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
35
97
|
let requestPayload;
|
|
36
98
|
|
|
37
99
|
// Serialise the rpc request.
|
|
@@ -51,45 +113,24 @@ function _prepareInvokeRequest(
|
|
|
51
113
|
method_invoke: {
|
|
52
114
|
method_name: this._grpcClient[method].path,
|
|
53
115
|
method_type: methodType,
|
|
54
|
-
token: methodToken.token,
|
|
55
116
|
request: requestPayload
|
|
56
117
|
}
|
|
57
118
|
};
|
|
58
119
|
|
|
59
|
-
let targetDID = [];
|
|
60
120
|
let invokePayload;
|
|
61
|
-
|
|
62
|
-
// When sending via a relay we need to encrypt the payload.
|
|
63
|
-
invokePayload = VoltCredential.aesEncrypt(
|
|
64
|
-
tunnelMethod.responseSerialize(tunnelResp),
|
|
65
|
-
methodToken.sharedKey.key,
|
|
66
|
-
methodToken.sharedKey.iv
|
|
67
|
-
);
|
|
68
|
-
} else {
|
|
69
|
-
invokePayload = tunnelMethod.responseSerialize(tunnelResp);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (this._voltClient.isVoltRelay) {
|
|
73
|
-
targetDID.push(this._voltClient.voltConfig.id);
|
|
74
|
-
}
|
|
121
|
+
const iv = aesCreateKey().iv;
|
|
75
122
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
log("target is %j", targetDID);
|
|
123
|
+
// When sending via a relay we need to encrypt the payload.
|
|
124
|
+
invokePayload = aesEncrypt(
|
|
125
|
+
this._encryptionKey,
|
|
126
|
+
iv,
|
|
127
|
+
tunnelMethod.responseSerialize(tunnelResp)
|
|
128
|
+
);
|
|
83
129
|
|
|
84
130
|
invokeRequest = {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
payload: invokePayload,
|
|
88
|
-
target_service_id: isServiceRelayed ? this._service.id : undefined
|
|
131
|
+
iv,
|
|
132
|
+
payload: invokePayload
|
|
89
133
|
};
|
|
90
|
-
|
|
91
|
-
// Replace the target method with a call to Invoke on the relay Volt.
|
|
92
|
-
callMethod = "Invoke";
|
|
93
134
|
} else {
|
|
94
135
|
invokeRequest = request;
|
|
95
136
|
}
|
|
@@ -109,7 +150,10 @@ function _preparePayloadRequest(request, invokeInfo) {
|
|
|
109
150
|
this._service?.service_description.host_type ===
|
|
110
151
|
"SERVICE_HOST_TYPE_RELAYED";
|
|
111
152
|
|
|
153
|
+
let iv;
|
|
112
154
|
if (this._voltClient.isVoltRelay || isServiceRelayed) {
|
|
155
|
+
iv = aesCreateKey().iv;
|
|
156
|
+
|
|
113
157
|
const requestPayload =
|
|
114
158
|
this._grpcClient[invokeInfo.methodName].requestSerialize(request);
|
|
115
159
|
|
|
@@ -122,13 +166,14 @@ function _preparePayloadRequest(request, invokeInfo) {
|
|
|
122
166
|
}
|
|
123
167
|
};
|
|
124
168
|
|
|
125
|
-
const invokePayload =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
169
|
+
const invokePayload = aesEncrypt(
|
|
170
|
+
this._encryptionKey,
|
|
171
|
+
iv,
|
|
172
|
+
tunnelMethod.responseSerialize(tunnelResp)
|
|
129
173
|
);
|
|
130
174
|
|
|
131
175
|
payloadRequest = {
|
|
176
|
+
iv,
|
|
132
177
|
payload: invokePayload
|
|
133
178
|
};
|
|
134
179
|
} else {
|
|
@@ -147,13 +192,38 @@ function _parseResponse(method, meta, response) {
|
|
|
147
192
|
"SERVICE_HOST_TYPE_RELAYED";
|
|
148
193
|
|
|
149
194
|
if (this._voltClient.isVoltRelay || isServiceRelayed) {
|
|
150
|
-
if (response.
|
|
195
|
+
if (response.key_exchange) {
|
|
196
|
+
const encryptionKey = response.key_exchange.encryption_key;
|
|
197
|
+
const nonce = response.key_exchange.nonce;
|
|
198
|
+
const signature = response.key_exchange.signature;
|
|
199
|
+
|
|
200
|
+
// The signed message is the encryption key and the nonce.
|
|
201
|
+
const message = new Uint8Array(encryptionKey.length + nonce.length);
|
|
202
|
+
message.set(encryptionKey);
|
|
203
|
+
message.set(nonce, encryptionKey.length);
|
|
204
|
+
|
|
205
|
+
// Check the signature using the target service public key.
|
|
206
|
+
const targetKey = keyFromPem(
|
|
207
|
+
this._service?.service_description?.host_public_key ||
|
|
208
|
+
this._voltClient.credential.voltPublicKey
|
|
209
|
+
).publicKey;
|
|
210
|
+
|
|
211
|
+
if (!verify(targetKey, message, signature)) {
|
|
212
|
+
throw new Error("signature verification failed");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
invokeResponse = {
|
|
216
|
+
key_exchange: {
|
|
217
|
+
encryption_key: formatPEM(toBase64(encryptionKey), "PUBLIC KEY")
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
} else if (response.payload) {
|
|
151
221
|
let decryptedPayload;
|
|
152
|
-
if (
|
|
153
|
-
decryptedPayload =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
222
|
+
if (this._encryptionKey) {
|
|
223
|
+
decryptedPayload = aesDecrypt(
|
|
224
|
+
this._encryptionKey,
|
|
225
|
+
response.iv,
|
|
226
|
+
response.payload
|
|
157
227
|
);
|
|
158
228
|
} else {
|
|
159
229
|
decryptedPayload = response.payload;
|
|
@@ -193,11 +263,11 @@ function _parseResponse(method, meta, response) {
|
|
|
193
263
|
invokeResponse.methodId = response.invoke_id;
|
|
194
264
|
log("started method %d for %s", invokeResponse.methodId, method);
|
|
195
265
|
}
|
|
196
|
-
} else if (this._voltClient.isRemote &&
|
|
197
|
-
const decryptedPayload =
|
|
198
|
-
|
|
199
|
-
meta.sharedKey.
|
|
200
|
-
|
|
266
|
+
} else if (this._voltClient.isRemote && this._encryptionKey) {
|
|
267
|
+
const decryptedPayload = aesDecrypt(
|
|
268
|
+
this._encryptionKey,
|
|
269
|
+
meta.sharedKey.iv,
|
|
270
|
+
response
|
|
201
271
|
);
|
|
202
272
|
invokeResponse = {
|
|
203
273
|
payload:
|
|
@@ -218,6 +288,9 @@ export default class GRPCCall extends EventEmitter {
|
|
|
218
288
|
this._call = null;
|
|
219
289
|
this._voltClient = voltClient;
|
|
220
290
|
this._service = service;
|
|
291
|
+
this._keyExchangePending = false;
|
|
292
|
+
this._pendingRequests = [];
|
|
293
|
+
this._encryptionKey = null;
|
|
221
294
|
}
|
|
222
295
|
|
|
223
296
|
start(grpcClient, request) {
|
|
@@ -246,7 +319,7 @@ export default class GRPCCall extends EventEmitter {
|
|
|
246
319
|
}
|
|
247
320
|
|
|
248
321
|
this._call = callClient[this._initialRequest.method](
|
|
249
|
-
this._initialRequest
|
|
322
|
+
this._initialRequest?.meta?.metadata
|
|
250
323
|
);
|
|
251
324
|
|
|
252
325
|
this._call.on("data", (streamResponse) => {
|
|
@@ -257,7 +330,25 @@ export default class GRPCCall extends EventEmitter {
|
|
|
257
330
|
this._initialRequest.meta,
|
|
258
331
|
streamResponse
|
|
259
332
|
);
|
|
260
|
-
if (parsedResponse.
|
|
333
|
+
if (parsedResponse.key_exchange) {
|
|
334
|
+
const peerKey = keyFromPem(
|
|
335
|
+
parsedResponse.key_exchange.encryption_key
|
|
336
|
+
).publicKey;
|
|
337
|
+
|
|
338
|
+
this._encryptionKey = deriveSharedKey(
|
|
339
|
+
this._initialRequest.meta.sharedKey.key,
|
|
340
|
+
peerKey,
|
|
341
|
+
x25519
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// We've exchanged keys, so send any queued payloads on the next tick.
|
|
345
|
+
process.nextTick(() => {
|
|
346
|
+
this._pendingRequests.forEach((pendingRequest) => {
|
|
347
|
+
this.write(pendingRequest);
|
|
348
|
+
});
|
|
349
|
+
this._pendingRequests = [];
|
|
350
|
+
});
|
|
351
|
+
} else if (parsedResponse.payload) {
|
|
261
352
|
// Interpret a non-empty status message as an error.
|
|
262
353
|
if (parsedResponse.payload?.status?.message) {
|
|
263
354
|
this.emit(
|
|
@@ -332,7 +423,7 @@ export default class GRPCCall extends EventEmitter {
|
|
|
332
423
|
if (!this._call) {
|
|
333
424
|
log("ERROR - call not initialised");
|
|
334
425
|
throw new Error("call not initialised");
|
|
335
|
-
} else if (this._initialRequest) {
|
|
426
|
+
} else if (this._initialRequest && !this._keyExchangePending) {
|
|
336
427
|
const payloadRequest = _preparePayloadRequest.call(
|
|
337
428
|
this,
|
|
338
429
|
request,
|
|
@@ -340,6 +431,8 @@ export default class GRPCCall extends EventEmitter {
|
|
|
340
431
|
);
|
|
341
432
|
return this._call.write(payloadRequest);
|
|
342
433
|
} else {
|
|
434
|
+
this._keyExchangePending = false;
|
|
435
|
+
|
|
343
436
|
this._initialRequest = _prepareInvokeRequest.call(
|
|
344
437
|
this,
|
|
345
438
|
request,
|
package/src/grpc-utils.js
CHANGED
|
@@ -20,8 +20,11 @@ export function createClient(
|
|
|
20
20
|
) {
|
|
21
21
|
let grpcCredentials;
|
|
22
22
|
if (credentials) {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const relaying = !!credentials.config?.volt?.relay;
|
|
24
|
+
const ca = relaying
|
|
25
|
+
? credentials.config?.volt?.relay?.ca_pem
|
|
26
|
+
: credentials.cache.ca;
|
|
27
|
+
const cert = relaying ? "" : credentials.cache.cert;
|
|
25
28
|
const { key } = credentials.cache;
|
|
26
29
|
|
|
27
30
|
if (key && cert) {
|
|
@@ -33,8 +36,9 @@ export function createClient(
|
|
|
33
36
|
tlsOptions.cert
|
|
34
37
|
);
|
|
35
38
|
} else {
|
|
36
|
-
// Cache has no private key, this implies we are connecting without supplying a client certificate.
|
|
37
|
-
//
|
|
39
|
+
// Cache has no private key or certificate, this implies we are connecting without supplying a client certificate.
|
|
40
|
+
// We might be connecting to a relay, or making an initial Authenticate call.
|
|
41
|
+
// Simply connect using the CA certificate, and the a JWT will be used to authenticate.
|
|
38
42
|
grpcCredentials = grpc.credentials.createSsl(Buffer.from(ca));
|
|
39
43
|
}
|
|
40
44
|
} else {
|
package/src/rpc-invocation.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import debug from "debug";
|
|
2
|
-
import { VoltCredential } from "./volt-credential.js";
|
|
3
2
|
import jwt from "jsonwebtoken";
|
|
4
3
|
import EventEmitter from "events";
|
|
4
|
+
import { voltUtils as voltClientUtils } from "@tdxvolt/volt-client-web/js";
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
aesDecrypt,
|
|
8
|
+
aesEncrypt,
|
|
9
|
+
createEncryptionKey,
|
|
10
|
+
deriveSharedKey,
|
|
11
|
+
formatPEM,
|
|
12
|
+
fromBase64,
|
|
13
|
+
fromBase64Url,
|
|
14
|
+
keyFromPem,
|
|
15
|
+
publicKeyToPem,
|
|
16
|
+
randomBytes,
|
|
17
|
+
signBase64,
|
|
18
|
+
stripPEMHeaders,
|
|
19
|
+
x25519
|
|
20
|
+
} = voltClientUtils;
|
|
5
21
|
|
|
6
22
|
const log = debug("rpc-invocation");
|
|
7
23
|
|
|
@@ -9,13 +25,13 @@ class RpcInvocation extends EventEmitter {
|
|
|
9
25
|
#voltClient = null;
|
|
10
26
|
#voltConnection = null;
|
|
11
27
|
#token = null;
|
|
12
|
-
#encryptKey = null;
|
|
13
|
-
#encryptIV = null;
|
|
14
28
|
#invokeId = null;
|
|
15
29
|
#payload = null;
|
|
16
30
|
#isJSON = false;
|
|
17
31
|
#methodDescriptor = null;
|
|
18
32
|
#methodName = null;
|
|
33
|
+
#derivedKey = null;
|
|
34
|
+
#started = false;
|
|
19
35
|
|
|
20
36
|
constructor(voltClient, voltConnection) {
|
|
21
37
|
super();
|
|
@@ -59,51 +75,89 @@ class RpcInvocation extends EventEmitter {
|
|
|
59
75
|
this.#methodDescriptor = methodDescriptor;
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
get started() {
|
|
79
|
+
return this.#started;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#keyExchange(token) {
|
|
63
83
|
// We don't verify the token at this point, as we don't know the public key.
|
|
64
84
|
// The rpc implementation should do the verification using the Volt API (e.g. CanAccessResource).
|
|
65
85
|
this.#token = jwt.decode(token);
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
// Store the JWT key id (kid) for use in the response.
|
|
88
|
+
const callerPublicKeyPem = formatPEM(this.#token.k, "PUBLIC KEY");
|
|
89
|
+
const callerKey = keyFromPem(callerPublicKeyPem, "", x25519).publicKey;
|
|
90
|
+
|
|
91
|
+
const ephemeralKeyPem = createEncryptionKey();
|
|
92
|
+
const ephemeralKey = keyFromPem(ephemeralKeyPem, "", x25519);
|
|
93
|
+
const ephemeralPublicKey = fromBase64(
|
|
94
|
+
stripPEMHeaders(publicKeyToPem(ephemeralKey.publicKey, x25519))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
this.#derivedKey = deriveSharedKey(
|
|
98
|
+
ephemeralKey.privateKey,
|
|
99
|
+
callerKey,
|
|
100
|
+
x25519
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const nonce = randomBytes(16);
|
|
104
|
+
|
|
105
|
+
// The signed message is the encryption key and the nonce.
|
|
106
|
+
const message = new Uint8Array(ephemeralPublicKey.length + nonce.length);
|
|
107
|
+
message.set(ephemeralPublicKey);
|
|
108
|
+
message.set(nonce, ephemeralPublicKey.length);
|
|
109
|
+
|
|
110
|
+
const signature = signBase64(
|
|
111
|
+
this.#voltClient.credential.getKey().privateKey,
|
|
112
|
+
message
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const keyExchangeResponse = {
|
|
116
|
+
invoke_id: this.#invokeId,
|
|
117
|
+
key_exchange: {
|
|
118
|
+
nonce,
|
|
119
|
+
encryption_key: ephemeralPublicKey,
|
|
120
|
+
signature: fromBase64Url(signature)
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
this.#voltConnection.send({
|
|
125
|
+
invoke_response: keyExchangeResponse
|
|
126
|
+
});
|
|
78
127
|
|
|
79
|
-
return Promise.resolve(
|
|
128
|
+
return Promise.resolve(keyExchangeResponse);
|
|
80
129
|
}
|
|
81
130
|
|
|
82
131
|
initialise(invoke_request) {
|
|
83
132
|
this.#invokeId = invoke_request.invoke_id;
|
|
84
133
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return this.parsePayload(invoke_request);
|
|
89
|
-
})
|
|
90
|
-
.catch((err) => {
|
|
134
|
+
if (!this.#derivedKey) {
|
|
135
|
+
// We don't have the derived key yet, so we need to perform the key exchange.
|
|
136
|
+
return this.#keyExchange(invoke_request.token).catch((err) => {
|
|
91
137
|
log("failure initialising invocation: %s", err.message);
|
|
92
138
|
return Promise.reject(err);
|
|
93
139
|
});
|
|
140
|
+
} else {
|
|
141
|
+
// We already have the derived key, so we can just parse the payload.
|
|
142
|
+
return this.parsePayload(invoke_request);
|
|
143
|
+
}
|
|
94
144
|
}
|
|
95
145
|
|
|
96
146
|
parsePayload(invoke_request) {
|
|
147
|
+
this.#started = true;
|
|
148
|
+
|
|
97
149
|
return new Promise((resolve, reject) => {
|
|
150
|
+
const encryptIV = invoke_request.iv;
|
|
151
|
+
|
|
98
152
|
if (invoke_request.payload) {
|
|
99
153
|
let decryptedPayload;
|
|
100
154
|
|
|
101
|
-
if (
|
|
155
|
+
if (encryptIV) {
|
|
102
156
|
// Decrypt the actual payload using the shared key and iv.
|
|
103
|
-
decryptedPayload =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
decryptedPayload = aesDecrypt(
|
|
158
|
+
this.#derivedKey,
|
|
159
|
+
encryptIV,
|
|
160
|
+
invoke_request.payload
|
|
107
161
|
);
|
|
108
162
|
} else {
|
|
109
163
|
// No encryption => just use the payload.
|
|
@@ -139,12 +193,12 @@ class RpcInvocation extends EventEmitter {
|
|
|
139
193
|
|
|
140
194
|
this.#isJSON = true;
|
|
141
195
|
|
|
142
|
-
if (this.#
|
|
196
|
+
if (this.#derivedKey) {
|
|
143
197
|
// Decrypt the actual payload using the shared key and iv.
|
|
144
|
-
decryptedPayload =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
198
|
+
decryptedPayload = aesDecrypt(
|
|
199
|
+
this.#derivedKey,
|
|
200
|
+
invoke_request.iv,
|
|
201
|
+
Buffer.from(invoke_request.json_payload, "base64")
|
|
148
202
|
);
|
|
149
203
|
} else {
|
|
150
204
|
// No encryption => just use the payload.
|
|
@@ -203,17 +257,15 @@ class RpcInvocation extends EventEmitter {
|
|
|
203
257
|
});
|
|
204
258
|
}
|
|
205
259
|
|
|
206
|
-
|
|
260
|
+
const iv = randomBytes(16);
|
|
261
|
+
if (this.#derivedKey) {
|
|
207
262
|
// Encrypt the response using the shared key and iv.
|
|
208
|
-
responsePayload =
|
|
209
|
-
responsePayload,
|
|
210
|
-
this.#encryptKey,
|
|
211
|
-
this.#encryptIV
|
|
212
|
-
);
|
|
263
|
+
responsePayload = aesEncrypt(this.#derivedKey, iv, responsePayload);
|
|
213
264
|
}
|
|
214
265
|
|
|
215
266
|
const invokeResponse = {
|
|
216
|
-
invoke_id: this.#invokeId
|
|
267
|
+
invoke_id: this.#invokeId,
|
|
268
|
+
iv
|
|
217
269
|
};
|
|
218
270
|
|
|
219
271
|
if (this.#isJSON) {
|
|
@@ -248,18 +300,16 @@ class RpcInvocation extends EventEmitter {
|
|
|
248
300
|
endPayload = tunnelMethod.requestSerialize(endPayload);
|
|
249
301
|
}
|
|
250
302
|
|
|
251
|
-
|
|
303
|
+
const iv = randomBytes(16);
|
|
304
|
+
if (this.#derivedKey) {
|
|
252
305
|
// Encrypt the response using the shared key and iv.
|
|
253
|
-
endPayload =
|
|
254
|
-
endPayload,
|
|
255
|
-
this.#encryptKey,
|
|
256
|
-
this.#encryptIV
|
|
257
|
-
);
|
|
306
|
+
endPayload = aesEncrypt(this.#derivedKey, iv, endPayload);
|
|
258
307
|
}
|
|
259
308
|
|
|
260
309
|
const invokeResponse = {
|
|
261
310
|
invoke_id: this.#invokeId,
|
|
262
|
-
server_end: true
|
|
311
|
+
server_end: true,
|
|
312
|
+
iv
|
|
263
313
|
};
|
|
264
314
|
|
|
265
315
|
if (this.#isJSON) {
|