@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.
@@ -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: "tdx-token",
4
- collectionOperation: {
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: "tdx-volt-config"
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 { VoltCredential } from "./volt-credential.js";
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
- true
51
+ relaying
25
52
  );
26
53
 
27
54
  let invokeRequest;
28
55
  let callMethod = method;
29
56
 
30
- if (this._voltClient.isVoltRelay || isServiceRelayed) {
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
- if (this._voltClient.isVoltRelay || isServiceRelayed) {
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
- if (isServiceRelayed) {
77
- // Add another relay hop if the target service is relayed.
78
- log("target service is relayed");
79
- targetDID.push(this._service.service_description.host_client_id);
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
- target_did: targetDID,
86
- token: methodToken.token,
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 = VoltCredential.aesEncrypt(
126
- tunnelMethod.responseSerialize(tunnelResp),
127
- invokeInfo.meta.sharedKey.key,
128
- invokeInfo.meta.sharedKey.iv
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.payload) {
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 (meta.sharedKey.key) {
153
- decryptedPayload = VoltCredential.aesDecrypt(
154
- response.payload,
155
- meta.sharedKey.key,
156
- meta.sharedKey.iv
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 && meta.sharedKey.key) {
197
- const decryptedPayload = VoltCredential.aesDecrypt(
198
- response,
199
- meta.sharedKey.key,
200
- meta.sharedKey.iv
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.meta.metadata
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.payload) {
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 ca = credentials.config?.volt?.relay?.ca_pem || credentials.cache.ca;
24
- const cert = credentials.cache.cloud_cert || credentials.cache.cert;
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
- // Simply connect using the CA certificate.
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 {
@@ -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
- #decodeToken(token) {
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
- if (this.#token.sk) {
68
- // Decrypt the shared key and iv using the private key.
69
- this.#encryptKey = VoltCredential.rsaDecrypt(
70
- this.#voltClient.credential.cache.key,
71
- this.#token.sk
72
- );
73
- this.#encryptIV = VoltCredential.rsaDecrypt(
74
- this.#voltClient.credential.cache.key,
75
- this.#token.iv
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(this.#token);
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
- return this.#decodeToken(invoke_request.token)
86
- .then(() => {
87
- // Parse the initial payload.
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 (this.#encryptKey) {
155
+ if (encryptIV) {
102
156
  // Decrypt the actual payload using the shared key and iv.
103
- decryptedPayload = VoltCredential.aesDecrypt(
104
- invoke_request.payload,
105
- this.#encryptKey,
106
- this.#encryptIV
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.#encryptKey) {
196
+ if (this.#derivedKey) {
143
197
  // Decrypt the actual payload using the shared key and iv.
144
- decryptedPayload = VoltCredential.aesDecrypt(
145
- Buffer.from(invoke_request.json_payload, "base64"),
146
- this.#encryptKey,
147
- this.#encryptIV
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
- if (this.#token.sk) {
260
+ const iv = randomBytes(16);
261
+ if (this.#derivedKey) {
207
262
  // Encrypt the response using the shared key and iv.
208
- responsePayload = VoltCredential.aesEncrypt(
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
- if (this.#token.sk) {
303
+ const iv = randomBytes(16);
304
+ if (this.#derivedKey) {
252
305
  // Encrypt the response using the shared key and iv.
253
- endPayload = VoltCredential.aesEncrypt(
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) {