1
1
import { type AuthProvider } from './auth/auth.js' ;
2
- import {
3
- ConfigurationError ,
4
- InvalidFileError ,
5
- NetworkError ,
6
- PermissionDeniedError ,
7
- ServiceError ,
8
- UnauthenticatedError ,
9
- } from './errors.js' ;
10
- import { pemToCryptoPublicKey , validateSecureUrl } from './utils.js' ;
2
+ import { ServiceError } from './errors.js' ;
3
+ import { RewrapResponse } from './platform/kas/kas_pb.js' ;
4
+ import { getPlatformUrlFromKasEndpoint , validateSecureUrl } from './utils.js' ;
5
+
6
+ import { fetchKeyAccessServers as fetchKeyAccessServersRpc } from './access/access-rpc.js' ;
7
+ import { fetchKeyAccessServers as fetchKeyAccessServersLegacy } from './access/access-fetch.js' ;
8
+ import { fetchWrappedKey as fetchWrappedKeysRpc } from './access/access-rpc.js' ;
9
+ import { fetchWrappedKey as fetchWrappedKeysLegacy } from './access/access-fetch.js' ;
10
+ import { fetchKasPubKey as fetchKasPubKeyRpc } from './access/access-rpc.js' ;
11
+ import { fetchKasPubKey as fetchKasPubKeyLegacy } from './access/access-fetch.js' ;
11
12
12
13
export type RewrapRequest = {
13
14
signedRequestToken : string ;
14
15
} ;
15
16
16
- export type RewrapResponse = {
17
- metadata : Record < string , unknown > ;
18
- entityWrappedKey : string ;
19
- sessionPublicKey : string ;
20
- schemaVersion : string ;
21
- } ;
22
-
23
17
/**
24
18
* Get a rewrapped access key to the document, if possible
25
19
* @param url Key access server rewrap endpoint
@@ -29,59 +23,20 @@ export type RewrapResponse = {
29
23
*/
30
24
export async function fetchWrappedKey (
31
25
url : string ,
32
- requestBody : RewrapRequest ,
33
- authProvider : AuthProvider ,
34
- clientVersion : string
26
+ signedRequestToken : string ,
27
+ authProvider : AuthProvider
35
28
) : Promise < RewrapResponse > {
36
- const req = await authProvider . withCreds ( {
37
- url,
38
- method : 'POST' ,
39
- headers : {
40
- 'Content-Type' : 'application/json' ,
41
- } ,
42
- body : JSON . stringify ( requestBody ) ,
43
- } ) ;
44
-
45
- let response : Response ;
46
-
47
- try {
48
- response = await fetch ( req . url , {
49
- method : req . method ,
50
- mode : 'cors' , // no-cors, *cors, same-origin
51
- cache : 'no-cache' , // *default, no-cache, reload, force-cache, only-if-cached
52
- credentials : 'same-origin' , // include, *same-origin, omit
53
- headers : req . headers ,
54
- redirect : 'follow' , // manual, *follow, error
55
- referrerPolicy : 'no-referrer' , // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
56
- body : req . body as BodyInit ,
57
- } ) ;
58
- } catch ( e ) {
59
- throw new NetworkError ( `unable to fetch wrapped key from [${ url } ]` , e ) ;
60
- }
61
-
62
- if ( ! response . ok ) {
63
- switch ( response . status ) {
64
- case 400 :
65
- throw new InvalidFileError (
66
- `400 for [${ req . url } ]: rewrap bad request [${ await response . text ( ) } ]`
67
- ) ;
68
- case 401 :
69
- throw new UnauthenticatedError ( `401 for [${ req . url } ]; rewrap auth failure` ) ;
70
- case 403 :
71
- throw new PermissionDeniedError ( `403 for [${ req . url } ]; rewrap permission denied` ) ;
72
- default :
73
- if ( response . status >= 500 ) {
74
- throw new ServiceError (
75
- `${ response . status } for [${ req . url } ]: rewrap failure due to service error [${ await response . text ( ) } ]`
76
- ) ;
77
- }
78
- throw new NetworkError (
79
- `${ req . method } ${ req . url } => ${ response . status } ${ response . statusText } `
80
- ) ;
81
- }
82
- }
83
-
84
- return response . json ( ) ;
29
+ const platformUrl = getPlatformUrlFromKasEndpoint ( url ) ;
30
+
31
+ return await tryPromisesUntilFirstSuccess (
32
+ ( ) => fetchWrappedKeysRpc ( platformUrl , signedRequestToken , authProvider ) ,
33
+ ( ) =>
34
+ fetchWrappedKeysLegacy (
35
+ url ,
36
+ { signedRequestToken } ,
37
+ authProvider
38
+ ) as unknown as Promise < RewrapResponse >
39
+ ) ;
85
40
}
86
41
87
42
export type KasPublicKeyAlgorithm = 'ec:secp256r1' | 'rsa:2048' ;
@@ -145,7 +100,7 @@ export type KasPublicKeyInfo = {
145
100
key : Promise < CryptoKey > ;
146
101
} ;
147
102
148
- async function noteInvalidPublicKey ( url : URL , r : Promise < CryptoKey > ) : Promise < CryptoKey > {
103
+ export async function noteInvalidPublicKey ( url : URL , r : Promise < CryptoKey > ) : Promise < CryptoKey > {
149
104
try {
150
105
return await r ;
151
106
} catch ( e ) {
@@ -160,49 +115,10 @@ export async function fetchKeyAccessServers(
160
115
platformUrl : string ,
161
116
authProvider : AuthProvider
162
117
) : Promise < OriginAllowList > {
163
- let nextOffset = 0 ;
164
- const allServers = [ ] ;
165
- do {
166
- const req = await authProvider . withCreds ( {
167
- url : `${ platformUrl } /key-access-servers?pagination.offset=${ nextOffset } ` ,
168
- method : 'GET' ,
169
- headers : {
170
- 'Content-Type' : 'application/json' ,
171
- } ,
172
- } ) ;
173
- let response : Response ;
174
- try {
175
- response = await fetch ( req . url , {
176
- method : req . method ,
177
- headers : req . headers ,
178
- body : req . body as BodyInit ,
179
- mode : 'cors' ,
180
- cache : 'no-cache' ,
181
- credentials : 'same-origin' ,
182
- redirect : 'follow' ,
183
- referrerPolicy : 'no-referrer' ,
184
- } ) ;
185
- } catch ( e ) {
186
- throw new NetworkError ( `unable to fetch kas list from [${ req . url } ]` , e ) ;
187
- }
188
- // if we get an error from the kas registry, throw an error
189
- if ( ! response . ok ) {
190
- throw new ServiceError (
191
- `unable to fetch kas list from [${ req . url } ], status: ${ response . status } `
192
- ) ;
193
- }
194
- const { keyAccessServers = [ ] , pagination = { } } = await response . json ( ) ;
195
- allServers . push ( ...keyAccessServers ) ;
196
- nextOffset = pagination . nextOffset || 0 ;
197
- } while ( nextOffset > 0 ) ;
198
-
199
- const serverUrls = allServers . map ( ( server ) => server . uri ) ;
200
- // add base platform kas
201
- if ( ! serverUrls . includes ( `${ platformUrl } /kas` ) ) {
202
- serverUrls . push ( `${ platformUrl } /kas` ) ;
203
- }
204
-
205
- return new OriginAllowList ( serverUrls , false ) ;
118
+ return await tryPromisesUntilFirstSuccess (
119
+ ( ) => fetchKeyAccessServersRpc ( platformUrl , authProvider ) ,
120
+ ( ) => fetchKeyAccessServersLegacy ( platformUrl , authProvider )
121
+ ) ;
206
122
}
207
123
208
124
/**
@@ -217,64 +133,10 @@ export async function fetchKasPubKey(
217
133
kasEndpoint : string ,
218
134
algorithm ?: KasPublicKeyAlgorithm
219
135
) : Promise < KasPublicKeyInfo > {
220
- if ( ! kasEndpoint ) {
221
- throw new ConfigurationError ( 'KAS definition not found' ) ;
222
- }
223
- // Logs insecure KAS. Secure is enforced in constructor
224
- validateSecureUrl ( kasEndpoint ) ;
225
-
226
- // Parse kasEndpoint to URL, then append to its path and update its query parameters
227
- let pkUrlV2 : URL ;
228
- try {
229
- pkUrlV2 = new URL ( kasEndpoint ) ;
230
- } catch ( e ) {
231
- throw new ConfigurationError ( `KAS definition invalid: [${ kasEndpoint } ]` , e ) ;
232
- }
233
- if ( ! pkUrlV2 . pathname . endsWith ( 'kas_public_key' ) ) {
234
- if ( ! pkUrlV2 . pathname . endsWith ( '/' ) ) {
235
- pkUrlV2 . pathname += '/' ;
236
- }
237
- pkUrlV2 . pathname += 'v2/kas_public_key' ;
238
- }
239
- pkUrlV2 . searchParams . set ( 'algorithm' , algorithm || 'rsa:2048' ) ;
240
- if ( ! pkUrlV2 . searchParams . get ( 'v' ) ) {
241
- pkUrlV2 . searchParams . set ( 'v' , '2' ) ;
242
- }
243
-
244
- let kasPubKeyResponseV2 : Response ;
245
- try {
246
- kasPubKeyResponseV2 = await fetch ( pkUrlV2 ) ;
247
- } catch ( e ) {
248
- throw new NetworkError ( `unable to fetch public key from [${ pkUrlV2 } ]` , e ) ;
249
- }
250
- if ( ! kasPubKeyResponseV2 . ok ) {
251
- switch ( kasPubKeyResponseV2 . status ) {
252
- case 404 :
253
- throw new ConfigurationError ( `404 for [${ pkUrlV2 } ]` ) ;
254
- case 401 :
255
- throw new UnauthenticatedError ( `401 for [${ pkUrlV2 } ]` ) ;
256
- case 403 :
257
- throw new PermissionDeniedError ( `403 for [${ pkUrlV2 } ]` ) ;
258
- default :
259
- throw new NetworkError (
260
- `${ pkUrlV2 } => ${ kasPubKeyResponseV2 . status } ${ kasPubKeyResponseV2 . statusText } `
261
- ) ;
262
- }
263
- }
264
- const jsonContent = await kasPubKeyResponseV2 . json ( ) ;
265
- const { publicKey, kid } : KasPublicKeyInfo = jsonContent ;
266
- if ( ! publicKey ) {
267
- throw new NetworkError (
268
- `invalid response from public key endpoint [${ JSON . stringify ( jsonContent ) } ]`
269
- ) ;
270
- }
271
- return {
272
- key : noteInvalidPublicKey ( pkUrlV2 , pemToCryptoPublicKey ( publicKey ) ) ,
273
- publicKey,
274
- url : kasEndpoint ,
275
- algorithm : algorithm || 'rsa:2048' ,
276
- ...( kid && { kid } ) ,
277
- } ;
136
+ return await tryPromisesUntilFirstSuccess (
137
+ ( ) => fetchKasPubKeyRpc ( kasEndpoint , algorithm ) ,
138
+ ( ) => fetchKasPubKeyLegacy ( kasEndpoint , algorithm )
139
+ ) ;
278
140
}
279
141
280
142
const origin = ( u : string ) : string => {
@@ -301,3 +163,25 @@ export class OriginAllowList {
301
163
return this . origins . includes ( origin ( url ) ) ;
302
164
}
303
165
}
166
+
167
+ /**
168
+ * Tries two promise-returning functions in order and returns the first successful result.
169
+ * If both fail, throws the error from the second.
170
+ * @param first First function returning a promise to try.
171
+ * @param second Second function returning a promise to try if the first fails.
172
+ */
173
+ async function tryPromisesUntilFirstSuccess < T > (
174
+ first : ( ) => Promise < T > ,
175
+ second : ( ) => Promise < T >
176
+ ) : Promise < T > {
177
+ try {
178
+ return await first ( ) ;
179
+ } catch ( e1 ) {
180
+ console . info ( 'v2 request error' , e1 ) ;
181
+ try {
182
+ return await second ( ) ;
183
+ } catch ( err ) {
184
+ throw err ;
185
+ }
186
+ }
187
+ }
0 commit comments