@tdxvolt/volt-client-grpc 0.16.0-beta.9 → 0.17.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/src/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // eslint-disable-next-line import/prefer-default-export
2
2
  export const constants = {
3
- authTokenName: "tdx-token",
3
+ authTokenName: "volt-token",
4
4
  collectionOperation: {
5
5
  add: "ADD",
6
6
  remove: "REMOVE",
@@ -23,9 +23,9 @@ export const constants = {
23
23
  ],
24
24
  userIdOID: "0.9.2342.19200300.100.1.1"
25
25
  },
26
- defaultDIDHostName: "cloud.tdxvolt.com",
26
+ defaultDIDHostName: "tdxvolt.com",
27
27
  didServiceType: {
28
- voltConfig: "tdx-volt-config"
28
+ voltConfig: "volt-discovery"
29
29
  },
30
30
  identityType: {
31
31
  did: "did",
@@ -36,8 +36,6 @@ export const constants = {
36
36
  online: "online",
37
37
  unknown: "unknown"
38
38
  },
39
- publicKeyPrefix: "-----BEGIN PUBLIC KEY-----",
40
- privateKeyPrefix: "-----BEGIN RSA PRIVATE KEY-----",
41
39
  privateEncryptedKeyPrefix: "-----BEGIN ENCRYPTED PRIVATE KEY-----",
42
40
  resource: {
43
41
  identity: "ident",
package/src/grpc-call.js CHANGED
@@ -1,7 +1,17 @@
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 {
5
+ aesCreateKey,
6
+ aesEncrypt,
7
+ aesDecrypt,
8
+ toBase64,
9
+ formatPEM,
10
+ keyFromPem,
11
+ verify,
12
+ deriveSharedKey,
13
+ x25519
14
+ } from "@tdxvolt/volt-client-web";
5
15
 
6
16
  const log = debug("volt-client-grpc:grpc-call");
7
17
 
@@ -14,6 +24,21 @@ function _prepareInvokeRequest(
14
24
  this._service?.service_description.host_type ===
15
25
  "SERVICE_HOST_TYPE_RELAYED";
16
26
 
27
+ const relaying = this._voltClient.isVoltRelay || isServiceRelayed;
28
+
29
+ let targetDID = [];
30
+ if (this._voltClient.isVoltRelay) {
31
+ targetDID.push(this._voltClient.voltConfig.id);
32
+ }
33
+
34
+ if (isServiceRelayed) {
35
+ // Add another relay hop if the target service is relayed.
36
+ log("target service is relayed");
37
+ targetDID.push(this._service.service_description.host_client_id);
38
+ }
39
+
40
+ log("target is %j", targetDID);
41
+
17
42
  // Use the service's host client id and public key if the service is relayed.
18
43
  const methodToken = this._voltClient.credential.getIdentityMetadata(
19
44
  this._voltClient.grpc,
@@ -21,17 +46,52 @@ function _prepareInvokeRequest(
21
46
  this._voltClient.voltConfig.id,
22
47
  this._service?.service_description.host_public_key ||
23
48
  this._voltClient.credential.voltPublicKey,
24
- true
49
+ relaying
25
50
  );
26
51
 
27
52
  let invokeRequest;
28
53
  let callMethod = method;
29
54
 
30
- if (this._voltClient.isVoltRelay || isServiceRelayed) {
55
+ if (relaying) {
31
56
  //
32
57
  // If the volt connection is via a relay (or the target service is relayed),
33
58
  // we wrap the rpc in a RemoteRequest message and send it via a call to Invoke().
34
59
  //
60
+ // Replace the target method with a call to Invoke on the relay Volt.
61
+ //
62
+ callMethod = "Invoke";
63
+
64
+ // But first we need to perform a key exchange with the target service.
65
+ //
66
+ if (!this._encryptionKey) {
67
+ // We haven't exchanged keys yet, so queue the payload.
68
+ this._pendingRequests.push(request);
69
+
70
+ if (this._keyExchangePending) {
71
+ // We've already sent the key exchange request, so do nothing.
72
+ log("key exchange pending, not sending payload");
73
+ return;
74
+ }
75
+
76
+ this._keyExchangePending = true;
77
+
78
+ // Send the key exchange request.
79
+ invokeRequest = {
80
+ token: methodToken.token,
81
+ target_did: targetDID,
82
+ target_service_id: isServiceRelayed
83
+ ? this._service.service_description.host_service_id
84
+ : undefined
85
+ };
86
+
87
+ return {
88
+ request: invokeRequest,
89
+ method: callMethod,
90
+ methodName: method,
91
+ meta: methodToken
92
+ };
93
+ }
94
+
35
95
  let requestPayload;
36
96
 
37
97
  // Serialise the rpc request.
@@ -51,45 +111,24 @@ function _prepareInvokeRequest(
51
111
  method_invoke: {
52
112
  method_name: this._grpcClient[method].path,
53
113
  method_type: methodType,
54
- token: methodToken.token,
55
114
  request: requestPayload
56
115
  }
57
116
  };
58
117
 
59
- let targetDID = [];
60
118
  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
- }
119
+ const iv = aesCreateKey().iv;
75
120
 
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);
121
+ // When sending via a relay we need to encrypt the payload.
122
+ invokePayload = aesEncrypt(
123
+ this._encryptionKey,
124
+ iv,
125
+ tunnelMethod.responseSerialize(tunnelResp)
126
+ );
83
127
 
84
128
  invokeRequest = {
85
- target_did: targetDID,
86
- token: methodToken.token,
87
- payload: invokePayload,
88
- target_service_id: isServiceRelayed ? this._service.id : undefined
129
+ iv,
130
+ payload: invokePayload
89
131
  };
90
-
91
- // Replace the target method with a call to Invoke on the relay Volt.
92
- callMethod = "Invoke";
93
132
  } else {
94
133
  invokeRequest = request;
95
134
  }
@@ -109,7 +148,10 @@ function _preparePayloadRequest(request, invokeInfo) {
109
148
  this._service?.service_description.host_type ===
110
149
  "SERVICE_HOST_TYPE_RELAYED";
111
150
 
151
+ let iv;
112
152
  if (this._voltClient.isVoltRelay || isServiceRelayed) {
153
+ iv = aesCreateKey().iv;
154
+
113
155
  const requestPayload =
114
156
  this._grpcClient[invokeInfo.methodName].requestSerialize(request);
115
157
 
@@ -122,13 +164,14 @@ function _preparePayloadRequest(request, invokeInfo) {
122
164
  }
123
165
  };
124
166
 
125
- const invokePayload = VoltCredential.aesEncrypt(
126
- tunnelMethod.responseSerialize(tunnelResp),
127
- invokeInfo.meta.sharedKey.key,
128
- invokeInfo.meta.sharedKey.iv
167
+ const invokePayload = aesEncrypt(
168
+ this._encryptionKey,
169
+ iv,
170
+ tunnelMethod.responseSerialize(tunnelResp)
129
171
  );
130
172
 
131
173
  payloadRequest = {
174
+ iv,
132
175
  payload: invokePayload
133
176
  };
134
177
  } else {
@@ -147,13 +190,38 @@ function _parseResponse(method, meta, response) {
147
190
  "SERVICE_HOST_TYPE_RELAYED";
148
191
 
149
192
  if (this._voltClient.isVoltRelay || isServiceRelayed) {
150
- if (response.payload) {
193
+ if (response.key_exchange) {
194
+ const encryptionKey = response.key_exchange.encryption_key;
195
+ const nonce = response.key_exchange.nonce;
196
+ const signature = response.key_exchange.signature;
197
+
198
+ // The signed message is the encryption key and the nonce.
199
+ const message = new Uint8Array(encryptionKey.length + nonce.length);
200
+ message.set(encryptionKey);
201
+ message.set(nonce, encryptionKey.length);
202
+
203
+ // Check the signature using the target service public key.
204
+ const targetKey = keyFromPem(
205
+ this._service?.service_description?.host_public_key ||
206
+ this._voltClient.credential.voltPublicKey
207
+ ).publicKey;
208
+
209
+ if (!verify(targetKey, message, signature)) {
210
+ throw new Error("signature verification failed");
211
+ }
212
+
213
+ invokeResponse = {
214
+ key_exchange: {
215
+ encryption_key: formatPEM(toBase64(encryptionKey), "PUBLIC KEY")
216
+ }
217
+ };
218
+ } else if (response.payload) {
151
219
  let decryptedPayload;
152
- if (meta.sharedKey.key) {
153
- decryptedPayload = VoltCredential.aesDecrypt(
154
- response.payload,
155
- meta.sharedKey.key,
156
- meta.sharedKey.iv
220
+ if (this._encryptionKey) {
221
+ decryptedPayload = aesDecrypt(
222
+ this._encryptionKey,
223
+ response.iv,
224
+ response.payload
157
225
  );
158
226
  } else {
159
227
  decryptedPayload = response.payload;
@@ -193,11 +261,11 @@ function _parseResponse(method, meta, response) {
193
261
  invokeResponse.methodId = response.invoke_id;
194
262
  log("started method %d for %s", invokeResponse.methodId, method);
195
263
  }
196
- } else if (this._voltClient.isRemote && meta.sharedKey.key) {
197
- const decryptedPayload = VoltCredential.aesDecrypt(
198
- response,
199
- meta.sharedKey.key,
200
- meta.sharedKey.iv
264
+ } else if (this._voltClient.isRemote && this._encryptionKey) {
265
+ const decryptedPayload = aesDecrypt(
266
+ this._encryptionKey,
267
+ meta.sharedKey.iv,
268
+ response
201
269
  );
202
270
  invokeResponse = {
203
271
  payload:
@@ -218,6 +286,9 @@ export default class GRPCCall extends EventEmitter {
218
286
  this._call = null;
219
287
  this._voltClient = voltClient;
220
288
  this._service = service;
289
+ this._keyExchangePending = false;
290
+ this._pendingRequests = [];
291
+ this._encryptionKey = null;
221
292
  }
222
293
 
223
294
  start(grpcClient, request) {
@@ -246,7 +317,7 @@ export default class GRPCCall extends EventEmitter {
246
317
  }
247
318
 
248
319
  this._call = callClient[this._initialRequest.method](
249
- this._initialRequest.meta.metadata
320
+ this._initialRequest?.meta?.metadata
250
321
  );
251
322
 
252
323
  this._call.on("data", (streamResponse) => {
@@ -257,7 +328,25 @@ export default class GRPCCall extends EventEmitter {
257
328
  this._initialRequest.meta,
258
329
  streamResponse
259
330
  );
260
- if (parsedResponse.payload) {
331
+ if (parsedResponse.key_exchange) {
332
+ const peerKey = keyFromPem(
333
+ parsedResponse.key_exchange.encryption_key
334
+ ).publicKey;
335
+
336
+ this._encryptionKey = deriveSharedKey(
337
+ this._initialRequest.meta.sharedKey.key,
338
+ peerKey,
339
+ x25519
340
+ );
341
+
342
+ // We've exchanged keys, so send any queued payloads on the next tick.
343
+ process.nextTick(() => {
344
+ this._pendingRequests.forEach((pendingRequest) => {
345
+ this.write(pendingRequest);
346
+ });
347
+ this._pendingRequests = [];
348
+ });
349
+ } else if (parsedResponse.payload) {
261
350
  // Interpret a non-empty status message as an error.
262
351
  if (parsedResponse.payload?.status?.message) {
263
352
  this.emit(
@@ -332,7 +421,7 @@ export default class GRPCCall extends EventEmitter {
332
421
  if (!this._call) {
333
422
  log("ERROR - call not initialised");
334
423
  throw new Error("call not initialised");
335
- } else if (this._initialRequest) {
424
+ } else if (this._initialRequest && !this._keyExchangePending) {
336
425
  const payloadRequest = _preparePayloadRequest.call(
337
426
  this,
338
427
  request,
@@ -340,6 +429,8 @@ export default class GRPCCall extends EventEmitter {
340
429
  );
341
430
  return this._call.write(payloadRequest);
342
431
  } else {
432
+ this._keyExchangePending = false;
433
+
343
434
  this._initialRequest = _prepareInvokeRequest.call(
344
435
  this,
345
436
  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,21 @@
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 {
5
+ aesDecrypt,
6
+ aesEncrypt,
7
+ createEncryptionKey,
8
+ deriveSharedKey,
9
+ formatPEM,
10
+ fromBase64,
11
+ fromBase64Url,
12
+ keyFromPem,
13
+ publicKeyToPem,
14
+ randomBytes,
15
+ signBase64,
16
+ stripPEMHeaders,
17
+ x25519
18
+ } from "@tdxvolt/volt-client-web";
5
19
 
6
20
  const log = debug("rpc-invocation");
7
21
 
@@ -9,13 +23,13 @@ class RpcInvocation extends EventEmitter {
9
23
  #voltClient = null;
10
24
  #voltConnection = null;
11
25
  #token = null;
12
- #encryptKey = null;
13
- #encryptIV = null;
14
26
  #invokeId = null;
15
27
  #payload = null;
16
28
  #isJSON = false;
17
29
  #methodDescriptor = null;
18
30
  #methodName = null;
31
+ #derivedKey = null;
32
+ #started = false;
19
33
 
20
34
  constructor(voltClient, voltConnection) {
21
35
  super();
@@ -59,51 +73,89 @@ class RpcInvocation extends EventEmitter {
59
73
  this.#methodDescriptor = methodDescriptor;
60
74
  }
61
75
 
62
- #decodeToken(token) {
76
+ get started() {
77
+ return this.#started;
78
+ }
79
+
80
+ #keyExchange(token) {
63
81
  // We don't verify the token at this point, as we don't know the public key.
64
82
  // The rpc implementation should do the verification using the Volt API (e.g. CanAccessResource).
65
83
  this.#token = jwt.decode(token);
66
84
 
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
- }
85
+ // Store the JWT key id (kid) for use in the response.
86
+ const callerPublicKeyPem = formatPEM(this.#token.k, "PUBLIC KEY");
87
+ const callerKey = keyFromPem(callerPublicKeyPem, "", x25519).publicKey;
88
+
89
+ const ephemeralKeyPem = createEncryptionKey();
90
+ const ephemeralKey = keyFromPem(ephemeralKeyPem, "", x25519);
91
+ const ephemeralPublicKey = fromBase64(
92
+ stripPEMHeaders(publicKeyToPem(ephemeralKey.publicKey, x25519))
93
+ );
94
+
95
+ this.#derivedKey = deriveSharedKey(
96
+ ephemeralKey.privateKey,
97
+ callerKey,
98
+ x25519
99
+ );
100
+
101
+ const nonce = randomBytes(16);
102
+
103
+ // The signed message is the encryption key and the nonce.
104
+ const message = new Uint8Array(ephemeralPublicKey.length + nonce.length);
105
+ message.set(ephemeralPublicKey);
106
+ message.set(nonce, ephemeralPublicKey.length);
107
+
108
+ const signature = signBase64(
109
+ this.#voltClient.credential.getKey().privateKey,
110
+ message
111
+ );
112
+
113
+ const keyExchangeResponse = {
114
+ invoke_id: this.#invokeId,
115
+ key_exchange: {
116
+ nonce,
117
+ encryption_key: ephemeralPublicKey,
118
+ signature: fromBase64Url(signature)
119
+ }
120
+ };
121
+
122
+ this.#voltConnection.send({
123
+ invoke_response: keyExchangeResponse
124
+ });
78
125
 
79
- return Promise.resolve(this.#token);
126
+ return Promise.resolve(keyExchangeResponse);
80
127
  }
81
128
 
82
129
  initialise(invoke_request) {
83
130
  this.#invokeId = invoke_request.invoke_id;
84
131
 
85
- return this.#decodeToken(invoke_request.token)
86
- .then(() => {
87
- // Parse the initial payload.
88
- return this.parsePayload(invoke_request);
89
- })
90
- .catch((err) => {
132
+ if (!this.#derivedKey) {
133
+ // We don't have the derived key yet, so we need to perform the key exchange.
134
+ return this.#keyExchange(invoke_request.token).catch((err) => {
91
135
  log("failure initialising invocation: %s", err.message);
92
136
  return Promise.reject(err);
93
137
  });
138
+ } else {
139
+ // We already have the derived key, so we can just parse the payload.
140
+ return this.parsePayload(invoke_request);
141
+ }
94
142
  }
95
143
 
96
144
  parsePayload(invoke_request) {
145
+ this.#started = true;
146
+
97
147
  return new Promise((resolve, reject) => {
148
+ const encryptIV = invoke_request.iv;
149
+
98
150
  if (invoke_request.payload) {
99
151
  let decryptedPayload;
100
152
 
101
- if (this.#encryptKey) {
153
+ if (encryptIV) {
102
154
  // Decrypt the actual payload using the shared key and iv.
103
- decryptedPayload = VoltCredential.aesDecrypt(
104
- invoke_request.payload,
105
- this.#encryptKey,
106
- this.#encryptIV
155
+ decryptedPayload = aesDecrypt(
156
+ this.#derivedKey,
157
+ encryptIV,
158
+ invoke_request.payload
107
159
  );
108
160
  } else {
109
161
  // No encryption => just use the payload.
@@ -139,12 +191,12 @@ class RpcInvocation extends EventEmitter {
139
191
 
140
192
  this.#isJSON = true;
141
193
 
142
- if (this.#encryptKey) {
194
+ if (this.#derivedKey) {
143
195
  // 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
196
+ decryptedPayload = aesDecrypt(
197
+ this.#derivedKey,
198
+ invoke_request.iv,
199
+ Buffer.from(invoke_request.json_payload, "base64")
148
200
  );
149
201
  } else {
150
202
  // No encryption => just use the payload.
@@ -203,17 +255,15 @@ class RpcInvocation extends EventEmitter {
203
255
  });
204
256
  }
205
257
 
206
- if (this.#token.sk) {
258
+ const iv = randomBytes(16);
259
+ if (this.#derivedKey) {
207
260
  // Encrypt the response using the shared key and iv.
208
- responsePayload = VoltCredential.aesEncrypt(
209
- responsePayload,
210
- this.#encryptKey,
211
- this.#encryptIV
212
- );
261
+ responsePayload = aesEncrypt(this.#derivedKey, iv, responsePayload);
213
262
  }
214
263
 
215
264
  const invokeResponse = {
216
- invoke_id: this.#invokeId
265
+ invoke_id: this.#invokeId,
266
+ iv
217
267
  };
218
268
 
219
269
  if (this.#isJSON) {
@@ -248,18 +298,16 @@ class RpcInvocation extends EventEmitter {
248
298
  endPayload = tunnelMethod.requestSerialize(endPayload);
249
299
  }
250
300
 
251
- if (this.#token.sk) {
301
+ const iv = randomBytes(16);
302
+ if (this.#derivedKey) {
252
303
  // Encrypt the response using the shared key and iv.
253
- endPayload = VoltCredential.aesEncrypt(
254
- endPayload,
255
- this.#encryptKey,
256
- this.#encryptIV
257
- );
304
+ endPayload = aesEncrypt(this.#derivedKey, iv, endPayload);
258
305
  }
259
306
 
260
307
  const invokeResponse = {
261
308
  invoke_id: this.#invokeId,
262
- server_end: true
309
+ server_end: true,
310
+ iv
263
311
  };
264
312
 
265
313
  if (this.#isJSON) {