@socialgouv/e2esdk-client
Advanced tools
Comparing version
@@ -11,3 +11,3 @@ import { PermissionFlags } from '@socialgouv/e2esdk-api'; | ||
}; | ||
type Config = Required<Omit<ClientConfig<Uint8Array>, 'handleSessionRefresh'>> & { | ||
type Config = Required<ClientConfig<Uint8Array>> & { | ||
clientId: string; | ||
@@ -92,5 +92,10 @@ }; | ||
enrollNewDevice(label?: string): Promise<string>; | ||
registerEnrolledDevice(qr: string): void; | ||
/** | ||
* Register this device and login | ||
* @param uri device registration URI | ||
*/ | ||
registerEnrolledDevice(uri: string): Promise<PublicUserIdentity | null>; | ||
get currentDeviceId(): string | null; | ||
getEnrolledDevices(): Promise<{ | ||
label: string | undefined; | ||
id: string; | ||
@@ -103,3 +108,2 @@ createdAt: string; | ||
enrolledFrom?: string | undefined; | ||
label?: string | undefined; | ||
}[]>; | ||
@@ -106,0 +110,0 @@ createKey(label: string, algorithm: 'secretBox' | 'sealedBox', expiresAt?: Date): Promise<KeychainItemMetadata>; |
import initOpaqueClient, { Registration, Login } from '@47ng/opaque-client'; | ||
import { wasmBase64 } from '@47ng/opaque-client/inline-wasm'; | ||
import { thirtyTwoBytesBase64Schema, sixtyFourBytesBase64Schema, fingerprintSchema, timestampSchema, identitySchema as identitySchema$1, signatureSchema, deviceIdSchema, base64Bytes, signupResponse, signupCompleteResponse, loginResponse, loginFinalResponse, deviceSchema, deviceEnrollmentResponse, deviceEnrolledResponse, listDevicesResponseBody, getSharedKeysResponseBody, getSingleIdentityResponseBody, getMultipleIdentitiesResponseBody, getParticipantsResponseBody, permissionFlags, getKeychainResponseBody, websocketNotificationTypesSchema, WebSocketNotificationTypes, responseHeaders, isFarFromCurrentTime } from '@socialgouv/e2esdk-api'; | ||
import { thirtyTwoBytesBase64Schema, sixtyFourBytesBase64Schema, fingerprintSchema, timestampSchema, identitySchema as identitySchema$1, signatureSchema, deviceIdSchema, base64Bytes, signupResponse, signupCompleteResponse, encodeDeviceRegistrationURI, loginResponse, loginFinalResponse, deviceSchema, deviceEnrollmentResponse, deviceEnrolledResponse, decodeDeviceRegistrationURI, listDevicesResponseBody, getSharedKeysResponseBody, getSingleIdentityResponseBody, getMultipleIdentitiesResponseBody, getParticipantsResponseBody, permissionFlags, getKeychainResponseBody, websocketNotificationTypesSchema, WebSocketNotificationTypes, responseHeaders, isFarFromCurrentTime } from '@socialgouv/e2esdk-api'; | ||
import { base64UrlDecode, cipherParser, sodium, deriveClientIdentity, base64UrlEncode, getOpaqueExportCipher, encrypt, fingerprint, decrypt, getDeviceLabelCipher, generateSealedBoxCipher, generateSecretBoxCipher, serializeCipher, encodedCiphertextFormatV1, randomPad, CIPHER_MAX_PADDED_LENGTH, multipartSignature, numberToUint32LE, verifyClientIdentity, decryptFormData, verifyMultipartSignature, memzeroCipher, signAuth, verifyAuth } from '@socialgouv/e2esdk-crypto'; | ||
@@ -11,2 +11,3 @@ import { LocalStateSync } from 'local-state-sync'; | ||
// src/index.ts | ||
var SESSION_REFRESH_RETRY_COUNT = 3; | ||
var NAME_PREFIX_LENGTH_BYTES = 32; | ||
@@ -69,3 +70,3 @@ var NAME_PREFIX_SEPARATOR = ":"; | ||
]); | ||
var deviceSecretSchema = base64Bytes(32); | ||
base64Bytes(32); | ||
var Client = class { | ||
@@ -79,3 +80,3 @@ sodium; | ||
#socketExponentialBackoffTimeout; | ||
#handleSessionRefresh; | ||
#sessionRefreshRetryCount; | ||
constructor(config) { | ||
@@ -91,5 +92,6 @@ const tick = performance.now(); | ||
handleNotifications: config.handleNotifications ?? true, | ||
handleSessionRefresh: config.handleSessionRefresh ?? true, | ||
clientId: typeof crypto === "object" ? crypto.randomUUID() : "not-available-in-ssr" | ||
}); | ||
this.#handleSessionRefresh = config.handleSessionRefresh ?? true; | ||
this.#sessionRefreshRetryCount = this.config.handleSessionRefresh ? SESSION_REFRESH_RETRY_COUNT : 0; | ||
this.#state = { | ||
@@ -179,3 +181,10 @@ state: "idle" | ||
registrationRecord, | ||
wrappedMainKey: encrypt(this.sodium, mainKey, mainKeyWrappingCipher), | ||
wrappedMainKey: encrypt( | ||
this.sodium, | ||
mainKey, | ||
mainKeyWrappingCipher, | ||
// Bind the ciphertext to the userId as authenticated additional data | ||
sodium.from_string(userId), | ||
"application/e2esdk.ciphertext.v1" | ||
), | ||
signaturePublicKey: base64UrlEncode(identity.signature.publicKey), | ||
@@ -200,3 +209,3 @@ sharingPublicKey: base64UrlEncode(identity.sharing.publicKey), | ||
this.registerEnrolledDevice( | ||
this.#encodeDeviceRegistrationQR(userId, deviceId, deviceSecret) | ||
encodeDeviceRegistrationURI(userId, deviceId, deviceSecret) | ||
); | ||
@@ -254,3 +263,10 @@ } catch (error) { | ||
const mainKeyWrappingCipher = getOpaqueExportCipher(this.sodium, exportKey); | ||
const mainKey = z.instanceof(Uint8Array).parse(decrypt(this.sodium, wrappedMainKey, mainKeyWrappingCipher)); | ||
const mainKey = z.instanceof(Uint8Array).parse( | ||
decrypt( | ||
this.sodium, | ||
wrappedMainKey, | ||
mainKeyWrappingCipher, | ||
this.sodium.from_string(userId) | ||
) | ||
); | ||
const identity = deriveClientIdentity(this.sodium, userId, mainKey); | ||
@@ -290,3 +306,8 @@ this.sodium.memzero(mainKey); | ||
const mainKey = z.instanceof(Uint8Array).parse( | ||
decrypt(this.sodium, device.wrappedMainKey, mainKeyUnwrappingCipher) | ||
decrypt( | ||
this.sodium, | ||
device.wrappedMainKey, | ||
mainKeyUnwrappingCipher, | ||
this.sodium.from_string(this.#state.identity.userId) | ||
) | ||
); | ||
@@ -332,3 +353,3 @@ this.sodium.memzero(mainKeyUnwrappingCipher.key); | ||
mainKeyRewrappingCipher, | ||
null, | ||
this.sodium.from_string(this.#state.identity.userId), | ||
"application/e2esdk.ciphertext.v1" | ||
@@ -364,3 +385,3 @@ ); | ||
); | ||
return this.#encodeDeviceRegistrationQR( | ||
return encodeDeviceRegistrationURI( | ||
this.#state.identity.userId, | ||
@@ -371,6 +392,17 @@ deviceId, | ||
} | ||
registerEnrolledDevice(qr) { | ||
const { userId, deviceId, deviceSecret } = this.#decodeDeviceRegistrationQR(qr); | ||
/** | ||
* Register this device and login | ||
* @param uri device registration URI | ||
*/ | ||
async registerEnrolledDevice(uri) { | ||
const { userId, deviceId, deviceSecret } = decodeDeviceRegistrationURI(uri); | ||
localStorage.setItem(`e2esdk:${userId}:device:id`, deviceId); | ||
localStorage.setItem(`e2esdk:${userId}:device:secret`, deviceSecret); | ||
try { | ||
return await this.login(userId); | ||
} catch (error) { | ||
localStorage.removeItem(`e2esdk:${userId}:device:id`); | ||
localStorage.removeItem(`e2esdk:${userId}:device:secret`); | ||
throw error; | ||
} | ||
} | ||
@@ -384,30 +416,22 @@ get currentDeviceId() { | ||
async getEnrolledDevices() { | ||
return listDevicesResponseBody.parse( | ||
await this.sodium.ready; | ||
if (this.#state.state !== "loaded") { | ||
throw new Error("Account locked: cannot list enrolled devices"); | ||
} | ||
const devices = listDevicesResponseBody.parse( | ||
await this.#apiCall("GET", "/v1/auth/devices") | ||
); | ||
} | ||
#encodeDeviceRegistrationQR(userId, deviceId, deviceSecret) { | ||
const qr = new URL("e2esdk://register-device"); | ||
qr.searchParams.set("userId", userId); | ||
qr.searchParams.set("deviceId", deviceId); | ||
qr.searchParams.set("deviceSecret", deviceSecret); | ||
return qr.toString(); | ||
} | ||
#decodeDeviceRegistrationQR(qr) { | ||
const url = new URL(qr); | ||
if (url.protocol !== "e2esdk:" || url.pathname !== "//register-device") { | ||
throw new Error("Invalid device registration data"); | ||
const deviceLabelCipher = getDeviceLabelCipher( | ||
this.sodium, | ||
this.#state.identity.userId, | ||
this.#state.identity.keychainBaseKey | ||
); | ||
try { | ||
return devices.map((device) => ({ | ||
...device, | ||
label: device.label ? z.string().parse(decrypt(this.sodium, device.label, deviceLabelCipher)) : void 0 | ||
})); | ||
} finally { | ||
this.sodium.memzero(deviceLabelCipher.key); | ||
} | ||
const userId = identitySchema.shape.userId.parse( | ||
url.searchParams.get("userId") | ||
); | ||
const deviceId = deviceIdSchema.parse(url.searchParams.get("deviceId")); | ||
const deviceSecret = deviceSecretSchema.parse( | ||
url.searchParams.get("deviceSecret") | ||
); | ||
return { | ||
userId, | ||
deviceId, | ||
deviceSecret | ||
}; | ||
} | ||
@@ -1170,18 +1194,16 @@ // Key Ops -- | ||
if (!res.ok) { | ||
if (this.#handleSessionRefresh && res.status === 401) { | ||
try { | ||
console.dir({ | ||
signatureItems, | ||
res: await res.json() | ||
}); | ||
throw new Error("Aborting infinite login recursion"); | ||
this.#handleSessionRefresh = false; | ||
return this.#apiCall(method, path, body); | ||
} finally { | ||
this.#handleSessionRefresh = true; | ||
if (res.status === 401) { | ||
if (this.#sessionRefreshRetryCount === 0) { | ||
const { error: statusText, statusCode, message } = await res.json(); | ||
throw new APIError(statusCode, statusText, message); | ||
} | ||
this.#sessionRefreshRetryCount--; | ||
await this.login(this.#state.identity.userId); | ||
return this.#apiCall(method, path, body); | ||
} else { | ||
const { error: statusText, statusCode, message } = await res.json(); | ||
throw new APIError(statusCode, statusText, message); | ||
} | ||
const { error: statusText, statusCode, message } = await res.json(); | ||
throw new APIError(statusCode, statusText, message); | ||
} | ||
this.#sessionRefreshRetryCount = this.config.handleSessionRefresh ? SESSION_REFRESH_RETRY_COUNT : 0; | ||
return this.#verifyServerResponse(method, res); | ||
@@ -1188,0 +1210,0 @@ } |
{ | ||
"name": "@socialgouv/e2esdk-client", | ||
"version": "1.0.0-beta.20", | ||
"version": "1.0.0-beta.21", | ||
"license": "Apache-2.0", | ||
@@ -5,0 +5,0 @@ "description": "End-to-end encryption client", |
@@ -1,1 +0,1 @@ | ||
{"$schema":"https://raw.githubusercontent.com/47ng/sceau/main/src/schemas/v1.schema.json","signature":"291f27e100f3d722fecbc960191ea204c4e60a020731b2f5201919919bbf1fbf92654e3261f4c7530c2ad08c3729a5b34be84d1aa902cca9ea9ef36f1f1b3507","publicKey":"82182691aa16fb18c4ee5f502f9067fe486768391d6ad5baa95e7a68913c9ad9","timestamp":"2023-03-22T13:58:56.225Z","sourceURL":"https://github.com/SocialGouv/e2esdk/tree/574e6ef8c41e71b8db078a59565e8e631823e9b6","buildURL":"https://github.com/SocialGouv/e2esdk/actions/runs/4490605706","manifest":[{"path":"README.md","hash":"4d045827bb62a85317c226a17beb8fcced4f7464c9cd98b1a9f5454f189d6a5e41da42f1b52183dfe66f166371e9caafb80f170af967d8c7b8999fecd6751c0b","sizeBytes":184,"signature":"73db42e6419b8c7b628e73c87229c182557c258a4ec4ed8a270cb01658fbf7b8d1289288d543bf6a8ec85c9f3836ef8d70f7cba18b38604b128b3973d5bcd009"},{"path":"dist/index.cjs","hash":"4341f05178c2e9cd723c826e4f55064bac85760e3f18998b0ca20a71be4f7bb0e27d70d0fb739ebefbbb9c8972532bacad72bae0493ee677ea8259d1a27332bf","sizeBytes":47805,"signature":"b309b243c786906eb27625fb90b4bd72f854dde0421ba65680ad4bf0c9e4d0b91d08b48815a8373e03ba597b2794bcb4e3fc5ef296576fc6664da43a70914406"},{"path":"dist/index.d.ts","hash":"ef070c076de9108201e24ee54b707ded3d4c4dc59996931af65bf7aa8a30b05e54343b80cbceeaf4233c55a0c95f68c7a5a0b1e3edf4c9eedc9a1a0877496487","sizeBytes":6077,"signature":"5cb33fc35e5e02302b093c16d3c360c74a28313bf5dabd5c723b83c651e72d5eaaf9f6274db5d2fdde69141973c16ed17d01e66f2b47acca14c91e573c3b7500"},{"path":"dist/index.js","hash":"c1cace74ad18c0c4d01f7a6a6690ca2fc06f6c2606f78109e06a99b67a356d20cfdffe39e6673dccd11891dfe2d3e39b5ef50e3fad3226091132a42652ac061c","sizeBytes":46520,"signature":"46b7fd11fcfe2a58b7831c74e900dc53c7bf2a9f0ecb8803630e6d309d304f4f641e678c126c841d3dc93f06be0e9b28b71e125bf7deaa667dbac1f07fc28005"},{"path":"package.json","hash":"12887414445b6856eb965c5ca9a44ff33cdd056af318470d8922d844c8cfd03a205c2279621b69a21a1a613e56738a7b3beb890887e0b2ec6ac8dbfa4ef5a623","sizeBytes":1352,"signature":"8535364ca8d04bf7d4cca17a3fb758148f7a623dc3b1e6d06f925f970fe0fb3ac2a43740fa867d74187400924dda1237ccd7df75f67784bb768a0ed0d86ece04"}]} | ||
{"$schema":"https://raw.githubusercontent.com/47ng/sceau/main/src/schemas/v1.schema.json","signature":"c91f990a274c789fc79ab890beb40adee4d79c31f5b895b7a804a2d3c3bc2085a263415d00ee0b28dd59e2015317a8f8e74b1e845eed2f7afeef8a1c3090360e","publicKey":"82182691aa16fb18c4ee5f502f9067fe486768391d6ad5baa95e7a68913c9ad9","timestamp":"2023-03-28T12:21:16.537Z","sourceURL":"https://github.com/SocialGouv/e2esdk/tree/beac0770dd9bc01df9bbe4256bb175adcacf4f7d","buildURL":"https://github.com/SocialGouv/e2esdk/actions/runs/4542914943","manifest":[{"path":"README.md","hash":"4d045827bb62a85317c226a17beb8fcced4f7464c9cd98b1a9f5454f189d6a5e41da42f1b52183dfe66f166371e9caafb80f170af967d8c7b8999fecd6751c0b","sizeBytes":184,"signature":"73db42e6419b8c7b628e73c87229c182557c258a4ec4ed8a270cb01658fbf7b8d1289288d543bf6a8ec85c9f3836ef8d70f7cba18b38604b128b3973d5bcd009"},{"path":"dist/index.cjs","hash":"05d0651e0f60339fd965c267348246b556fb8ad66691da5d3638762e5706942d4d013f7ed1b6d7ea05855a57d8e1b766b32a43f78f88ca77bfe73e78754114e4","sizeBytes":48540,"signature":"30aacc4fa112e368009ff314a0be6c81a089973ddfe83776753edba1e13f33e1ef4d34f1a92385c8fd6d57dac0d852c8aafd1787dd5f8d0b2bfd4cafc9791c06"},{"path":"dist/index.d.ts","hash":"156c75711d80b547ddfa759c9d67e7bdba8de383ee092b396c88ca8cde796d070bd39df7cc303939be1f1692746867b6358ca2d6e7d4293110f0e99404c9bc15","sizeBytes":6173,"signature":"2e626744ce8fe5eb6bfaed6c35aa86078af4cf493125669bcc0743b441e785f5092e3940659cac8102146e1e5ffdbe6ba82d4839e4ba74d86ce76488a4d47105"},{"path":"dist/index.js","hash":"07f5d52076c39fafb430344efbf45d298e56e3e672225d7d308fb516bcaed879d53651bb6a52452b568d750c27790590724cba883f16c36bbdcb25eae5fa295a","sizeBytes":47250,"signature":"2e543f57c15db87d8f8b440c9ebd6f9afae2949fd53f231965026eef59104ab8fa2687db938ed5401c74f9790198a011ae0f15ef1e869de5deae219e9436ba08"},{"path":"package.json","hash":"96dc001e5b9cdd99d1b87aaf49e4f3016298c265ca38e81cf64c0222ff06f59bec18ccc054d0995fbb671d4510f3e71db67fc84093d56ebd9c11de096fa3b06e","sizeBytes":1352,"signature":"7455584d3922864e9f444ced6421755d38c601a318ae51c086ff3d3d28554800369560bed6ceea3850bdf83efbbb1000e2471d43b13f7fd3245f27db8529670a"}]} |
Sorry, the diff of this file is not supported yet
105648
1.5%2929
1.67%