@travelclw/proof-protocol 0.1.1 → 0.2.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 +81 -65
- package/browser.d.ts +42 -40
- package/browser.js +342 -267
- package/core.js +221 -219
- package/node.d.ts +80 -76
- package/package.json +1 -10
- package/server.js +316 -288
package/browser.js
CHANGED
|
@@ -1,267 +1,342 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
canonicalizeBody,
|
|
5
|
-
canonicalizeQuery,
|
|
6
|
-
isRequestProofRequired,
|
|
7
|
-
isRequestProofSetupError,
|
|
8
|
-
normalizePath,
|
|
9
|
-
} = require('./core')
|
|
10
|
-
|
|
11
|
-
const bytesToBase64Url = bytes => {
|
|
12
|
-
let binary = ''
|
|
13
|
-
bytes.forEach(byte => {
|
|
14
|
-
binary += String.fromCharCode(byte)
|
|
15
|
-
})
|
|
16
|
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const textToBase64Url = value => bytesToBase64Url(new TextEncoder().encode(value))
|
|
20
|
-
|
|
21
|
-
const getWebCrypto = () => {
|
|
22
|
-
const webCrypto = globalThis.crypto
|
|
23
|
-
if (!webCrypto?.subtle) throw new Error('request_proof_unsupported')
|
|
24
|
-
return webCrypto
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const sha256Base64Url = async value => {
|
|
28
|
-
const digest = await getWebCrypto().subtle.digest('SHA-256', new TextEncoder().encode(value))
|
|
29
|
-
return bytesToBase64Url(new Uint8Array(digest))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const normalizePublicKey = publicKey => ({
|
|
33
|
-
kty: 'EC',
|
|
34
|
-
crv: 'P-256',
|
|
35
|
-
x: String(publicKey?.x || ''),
|
|
36
|
-
y: String(publicKey?.y || ''),
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const calculateKeyId = publicKey =>
|
|
40
|
-
sha256Base64Url(
|
|
41
|
-
JSON.stringify({
|
|
42
|
-
crv: 'P-256',
|
|
43
|
-
kty: 'EC',
|
|
44
|
-
x: String(publicKey?.x || ''),
|
|
45
|
-
y: String(publicKey?.y || ''),
|
|
46
|
-
}),
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
const createProofRequestId = () => {
|
|
50
|
-
const webCrypto = getWebCrypto()
|
|
51
|
-
return typeof webCrypto.randomUUID === 'function'
|
|
52
|
-
? webCrypto.randomUUID()
|
|
53
|
-
: bytesToBase64Url(webCrypto.getRandomValues(new Uint8Array(24)))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const createBrowserProofClient = options => {
|
|
57
|
-
const databaseName = String(options?.databaseName || '').trim()
|
|
58
|
-
const storeName = String(options?.storeName || 'context').trim()
|
|
59
|
-
const recordId = String(options?.recordId || 'request-proof-key').trim()
|
|
60
|
-
const required = () => isRequestProofRequired(options?.required?.())
|
|
61
|
-
let proofKeyPromise = null
|
|
62
|
-
let resetPromise = null
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
request
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
request.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
request
|
|
91
|
-
request.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
record.privateKey.
|
|
104
|
-
|
|
105
|
-
algorithm?.
|
|
106
|
-
|
|
107
|
-
record.privateKey.usages
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
publicKey,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
readRequest
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
transaction.
|
|
144
|
-
transaction.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
canonicalizeBody,
|
|
5
|
+
canonicalizeQuery,
|
|
6
|
+
isRequestProofRequired,
|
|
7
|
+
isRequestProofSetupError,
|
|
8
|
+
normalizePath,
|
|
9
|
+
} = require('./core')
|
|
10
|
+
|
|
11
|
+
const bytesToBase64Url = bytes => {
|
|
12
|
+
let binary = ''
|
|
13
|
+
bytes.forEach(byte => {
|
|
14
|
+
binary += String.fromCharCode(byte)
|
|
15
|
+
})
|
|
16
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const textToBase64Url = value => bytesToBase64Url(new TextEncoder().encode(value))
|
|
20
|
+
|
|
21
|
+
const getWebCrypto = () => {
|
|
22
|
+
const webCrypto = globalThis.crypto
|
|
23
|
+
if (!webCrypto?.subtle) throw new Error('request_proof_unsupported')
|
|
24
|
+
return webCrypto
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sha256Base64Url = async value => {
|
|
28
|
+
const digest = await getWebCrypto().subtle.digest('SHA-256', new TextEncoder().encode(value))
|
|
29
|
+
return bytesToBase64Url(new Uint8Array(digest))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizePublicKey = publicKey => ({
|
|
33
|
+
kty: 'EC',
|
|
34
|
+
crv: 'P-256',
|
|
35
|
+
x: String(publicKey?.x || ''),
|
|
36
|
+
y: String(publicKey?.y || ''),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const calculateKeyId = publicKey =>
|
|
40
|
+
sha256Base64Url(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
crv: 'P-256',
|
|
43
|
+
kty: 'EC',
|
|
44
|
+
x: String(publicKey?.x || ''),
|
|
45
|
+
y: String(publicKey?.y || ''),
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const createProofRequestId = () => {
|
|
50
|
+
const webCrypto = getWebCrypto()
|
|
51
|
+
return typeof webCrypto.randomUUID === 'function'
|
|
52
|
+
? webCrypto.randomUUID()
|
|
53
|
+
: bytesToBase64Url(webCrypto.getRandomValues(new Uint8Array(24)))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const createBrowserProofClient = options => {
|
|
57
|
+
const databaseName = String(options?.databaseName || '').trim()
|
|
58
|
+
const storeName = String(options?.storeName || 'context').trim()
|
|
59
|
+
const recordId = String(options?.recordId || 'request-proof-key').trim()
|
|
60
|
+
const required = () => isRequestProofRequired(options?.required?.())
|
|
61
|
+
let proofKeyPromise = null
|
|
62
|
+
let resetPromise = null
|
|
63
|
+
let resetChannel = null
|
|
64
|
+
|
|
65
|
+
if (!databaseName || !storeName || !recordId) {
|
|
66
|
+
throw new Error('request_proof_storage_failed')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const openDatabase = () =>
|
|
70
|
+
new Promise((resolve, reject) => {
|
|
71
|
+
if (!globalThis.indexedDB) {
|
|
72
|
+
reject(new Error('request_proof_storage_unsupported'))
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const request = globalThis.indexedDB.open(databaseName, 1)
|
|
76
|
+
request.onupgradeneeded = () => {
|
|
77
|
+
const database = request.result
|
|
78
|
+
if (!database.objectStoreNames.contains(storeName)) {
|
|
79
|
+
database.createObjectStore(storeName, { keyPath: 'id' })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
request.onsuccess = () => resolve(request.result)
|
|
83
|
+
request.onerror = () => reject(new Error('request_proof_storage_failed'))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const readStoredKey = async () => {
|
|
87
|
+
const database = await openDatabase()
|
|
88
|
+
try {
|
|
89
|
+
return await new Promise((resolve, reject) => {
|
|
90
|
+
const request = database.transaction(storeName, 'readonly').objectStore(storeName).get(recordId)
|
|
91
|
+
request.onsuccess = () => resolve(request.result || null)
|
|
92
|
+
request.onerror = () => reject(new Error('request_proof_read_failed'))
|
|
93
|
+
})
|
|
94
|
+
} finally {
|
|
95
|
+
database.close()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isStoredKeyValid = record => {
|
|
100
|
+
if (!record?.keyId || !record.publicKey?.x || !record.publicKey?.y || !record.privateKey) return false
|
|
101
|
+
const algorithm = record.privateKey.algorithm
|
|
102
|
+
return (
|
|
103
|
+
record.privateKey.type === 'private' &&
|
|
104
|
+
record.privateKey.extractable === false &&
|
|
105
|
+
algorithm?.name === 'ECDSA' &&
|
|
106
|
+
algorithm?.namedCurve === 'P-256' &&
|
|
107
|
+
Array.isArray(record.privateKey.usages) &&
|
|
108
|
+
record.privateKey.usages.includes('sign')
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const generateCandidate = async () => {
|
|
113
|
+
const webCrypto = getWebCrypto()
|
|
114
|
+
const keyPair = await webCrypto.subtle.generateKey(
|
|
115
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
116
|
+
false,
|
|
117
|
+
['sign', 'verify'],
|
|
118
|
+
)
|
|
119
|
+
const publicKey = normalizePublicKey(await webCrypto.subtle.exportKey('jwk', keyPair.publicKey))
|
|
120
|
+
return {
|
|
121
|
+
id: recordId,
|
|
122
|
+
keyId: await calculateKeyId(publicKey),
|
|
123
|
+
publicKey,
|
|
124
|
+
privateKey: keyPair.privateKey,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const storeIfAbsent = async candidate => {
|
|
130
|
+
const database = await openDatabase()
|
|
131
|
+
let selectedKey = candidate
|
|
132
|
+
try {
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
const transaction = database.transaction(storeName, 'readwrite')
|
|
135
|
+
const store = transaction.objectStore(storeName)
|
|
136
|
+
const readRequest = store.get(recordId)
|
|
137
|
+
readRequest.onsuccess = () => {
|
|
138
|
+
const storedKey = readRequest.result || null
|
|
139
|
+
if (isStoredKeyValid(storedKey)) selectedKey = storedKey
|
|
140
|
+
else store.put(candidate)
|
|
141
|
+
}
|
|
142
|
+
readRequest.onerror = () => reject(new Error('request_proof_read_failed'))
|
|
143
|
+
transaction.oncomplete = () => resolve()
|
|
144
|
+
transaction.onerror = () => reject(new Error('request_proof_write_failed'))
|
|
145
|
+
transaction.onabort = () => reject(new Error('request_proof_write_failed'))
|
|
146
|
+
})
|
|
147
|
+
return selectedKey
|
|
148
|
+
} finally {
|
|
149
|
+
database.close()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const getResetChannelName = () =>
|
|
154
|
+
`@travelclw/proof-protocol/reset/${databaseName}/${storeName}/${recordId}`
|
|
155
|
+
|
|
156
|
+
const invalidateLocalKey = () => {
|
|
157
|
+
proofKeyPromise = null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof globalThis.BroadcastChannel === 'function') {
|
|
161
|
+
try {
|
|
162
|
+
resetChannel = new globalThis.BroadcastChannel(getResetChannelName())
|
|
163
|
+
resetChannel.onmessage = event => {
|
|
164
|
+
if (event?.data?.type === 'request-proof-reset') invalidateLocalKey()
|
|
165
|
+
}
|
|
166
|
+
if (typeof resetChannel.unref === 'function') resetChannel.unref()
|
|
167
|
+
} catch {
|
|
168
|
+
resetChannel = null
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const broadcastReset = () => {
|
|
173
|
+
try {
|
|
174
|
+
resetChannel?.postMessage({ type: 'request-proof-reset' })
|
|
175
|
+
} catch {
|
|
176
|
+
// IndexedDB verification below remains authoritative when the channel is unavailable.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const getOrCreateKey = () => {
|
|
181
|
+
if (!proofKeyPromise) {
|
|
182
|
+
const pendingReset = resetPromise
|
|
183
|
+
const operation = (async () => {
|
|
184
|
+
await pendingReset
|
|
185
|
+
const storedKey = await readStoredKey()
|
|
186
|
+
if (isStoredKeyValid(storedKey)) return storedKey
|
|
187
|
+
return storeIfAbsent(await generateCandidate())
|
|
188
|
+
})()
|
|
189
|
+
const pendingKey = operation.finally(() => {
|
|
190
|
+
if (proofKeyPromise === pendingKey) proofKeyPromise = null
|
|
191
|
+
})
|
|
192
|
+
proofKeyPromise = pendingKey
|
|
193
|
+
}
|
|
194
|
+
return proofKeyPromise
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const getCurrentKey = async () => {
|
|
198
|
+
while (true) {
|
|
199
|
+
const key = await getOrCreateKey()
|
|
200
|
+
const storedKey = await readStoredKey()
|
|
201
|
+
if (isStoredKeyValid(storedKey) && storedKey.keyId === key.keyId) return storedKey
|
|
202
|
+
invalidateLocalKey()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const reset = () => {
|
|
207
|
+
const pendingReset = resetPromise
|
|
208
|
+
const operation = (async () => {
|
|
209
|
+
broadcastReset()
|
|
210
|
+
const pendingKey = proofKeyPromise
|
|
211
|
+
invalidateLocalKey()
|
|
212
|
+
await pendingReset?.catch(() => undefined)
|
|
213
|
+
await pendingKey?.catch(() => undefined)
|
|
214
|
+
if (!globalThis.indexedDB) return
|
|
215
|
+
|
|
216
|
+
const database = await openDatabase()
|
|
217
|
+
try {
|
|
218
|
+
await new Promise((resolve, reject) => {
|
|
219
|
+
const transaction = database.transaction(storeName, 'readwrite')
|
|
220
|
+
transaction.objectStore(storeName).delete(recordId)
|
|
221
|
+
transaction.oncomplete = () => resolve()
|
|
222
|
+
transaction.onerror = () => reject(new Error('request_proof_reset_failed'))
|
|
223
|
+
transaction.onabort = () => reject(new Error('request_proof_reset_failed'))
|
|
224
|
+
})
|
|
225
|
+
} finally {
|
|
226
|
+
database.close()
|
|
227
|
+
}
|
|
228
|
+
})()
|
|
229
|
+
|
|
230
|
+
resetPromise = operation
|
|
231
|
+
return operation.finally(() => {
|
|
232
|
+
if (resetPromise === operation) resetPromise = null
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const getRegistration = async () => {
|
|
237
|
+
const key = await getCurrentKey()
|
|
238
|
+
return { keyId: key.keyId, publicKey: key.publicKey }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const runtimeUnavailable = () =>
|
|
242
|
+
(typeof window !== 'undefined' && window.isSecureContext === false) ||
|
|
243
|
+
!globalThis.crypto?.subtle ||
|
|
244
|
+
!globalThis.indexedDB
|
|
245
|
+
|
|
246
|
+
const tryGetRegistration = async () => {
|
|
247
|
+
if (!required() && runtimeUnavailable()) return null
|
|
248
|
+
try {
|
|
249
|
+
return await getRegistration()
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (!required() && isRequestProofSetupError(error)) return null
|
|
252
|
+
throw error
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const createContextProof = async input => {
|
|
257
|
+
if (!['session', 'ticket'].includes(input.purpose)) throw new Error('request_proof_context_invalid')
|
|
258
|
+
const key = await readStoredKey()
|
|
259
|
+
if (!isStoredKeyValid(key) || resetPromise) throw new Error('request_proof_key_missing')
|
|
260
|
+
if (input.purpose === 'ticket' && key.keyId !== input.expectedKeyId) {
|
|
261
|
+
throw new Error('request_proof_key_changed')
|
|
262
|
+
}
|
|
263
|
+
let binding
|
|
264
|
+
if (input.purpose === 'session') {
|
|
265
|
+
try {
|
|
266
|
+
const header = JSON.parse(atob(String(input.token).split('.')[0].replace(/-/g, '+').replace(/_/g, '/')))
|
|
267
|
+
if (typeof header.psid !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(header.psid)) throw new Error('missing')
|
|
268
|
+
binding = { psid: header.psid }
|
|
269
|
+
} catch { throw new Error('request_proof_session_missing') }
|
|
270
|
+
} else {
|
|
271
|
+
if (!input.token) throw new Error('request_proof_context_invalid')
|
|
272
|
+
binding = { sth: await sha256Base64Url(input.token) }
|
|
273
|
+
}
|
|
274
|
+
const header = { alg: 'ES256', kid: key.keyId, typ: 'ctx-proof+jwt' }
|
|
275
|
+
const payload = {
|
|
276
|
+
ver: 2, purpose: input.purpose, ...binding,
|
|
277
|
+
aud: input.audience, htm: String(input.method).toUpperCase(), htu: normalizePath(input.path),
|
|
278
|
+
iat: Math.floor(Date.now() / 1000), jti: createProofRequestId(),
|
|
279
|
+
}
|
|
280
|
+
const signingInput = `${textToBase64Url(JSON.stringify(header))}.${textToBase64Url(JSON.stringify(payload))}`
|
|
281
|
+
const signature = await getWebCrypto().subtle.sign(
|
|
282
|
+
{ name: 'ECDSA', hash: 'SHA-256' }, key.privateKey, new TextEncoder().encode(signingInput),
|
|
283
|
+
)
|
|
284
|
+
const current = await readStoredKey()
|
|
285
|
+
if (resetPromise || !isStoredKeyValid(current) || current.keyId !== key.keyId) {
|
|
286
|
+
throw new Error('request_proof_key_changed')
|
|
287
|
+
}
|
|
288
|
+
return `${signingInput}.${bytesToBase64Url(new Uint8Array(signature))}`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const createProof = async input => {
|
|
292
|
+
if (input?.purpose) return createContextProof(input)
|
|
293
|
+
const token = String(input?.token || '').trim()
|
|
294
|
+
if (!token) return ''
|
|
295
|
+
const requestId = String(input?.requestId || '').trim()
|
|
296
|
+
const [ath, qsh, bth] = await Promise.all([
|
|
297
|
+
sha256Base64Url(token),
|
|
298
|
+
sha256Base64Url(canonicalizeQuery(input?.requestUri)),
|
|
299
|
+
sha256Base64Url(canonicalizeBody(input?.body, input?.contentType, input?.hasBody)),
|
|
300
|
+
])
|
|
301
|
+
const key = await getCurrentKey()
|
|
302
|
+
const header = { alg: 'ES256', kid: key.keyId, typ: 'dpop+jwt' }
|
|
303
|
+
const payload = {
|
|
304
|
+
ath,
|
|
305
|
+
aud: String(input?.audience || ''),
|
|
306
|
+
htm: String(input?.method || 'GET').toUpperCase(),
|
|
307
|
+
htu: normalizePath(input?.path),
|
|
308
|
+
qsh,
|
|
309
|
+
bth,
|
|
310
|
+
rid: requestId,
|
|
311
|
+
iat: Math.floor(Date.now() / 1000),
|
|
312
|
+
jti: createProofRequestId(),
|
|
313
|
+
}
|
|
314
|
+
const signingInput = `${textToBase64Url(JSON.stringify(header))}.${textToBase64Url(JSON.stringify(payload))}`
|
|
315
|
+
let signature
|
|
316
|
+
try {
|
|
317
|
+
signature = await getWebCrypto().subtle.sign(
|
|
318
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
319
|
+
key.privateKey,
|
|
320
|
+
new TextEncoder().encode(signingInput),
|
|
321
|
+
)
|
|
322
|
+
} catch {
|
|
323
|
+
throw new Error('request_proof_sign_failed')
|
|
324
|
+
}
|
|
325
|
+
return `${signingInput}.${bytesToBase64Url(new Uint8Array(signature))}`
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const tryCreateProof = async input => {
|
|
329
|
+
if (!String(input?.token || '').trim()) return ''
|
|
330
|
+
if (!input?.purpose && !required() && runtimeUnavailable()) return ''
|
|
331
|
+
try {
|
|
332
|
+
return await createProof(input)
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (!input?.purpose && !required() && isRequestProofSetupError(error)) return ''
|
|
335
|
+
throw error
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { createProof, getRegistration, reset, tryCreateProof, tryGetRegistration }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = { calculateKeyId, createBrowserProofClient, createProofRequestId }
|