diff --git a/.github/workflows/cd-cloudflare.yml b/.github/workflows/cd-cloudflare.yml index 31aa7273ab..50b6dd73dc 100644 --- a/.github/workflows/cd-cloudflare.yml +++ b/.github/workflows/cd-cloudflare.yml @@ -12,7 +12,7 @@ jobs: packages: read strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index c48aaa77c8..b74fbbcedc 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20.11.1' + node-version: '20.19.0' - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc diff --git a/package.json b/package.json index 9f14d4c2a9..afd330d8b1 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,12 @@ "@iconscout/react-unicons": "^1.1.6", "@internxt/css-config": "1.1.0", "@internxt/lib": "1.4.1", - "@internxt/sdk": "=1.11.17", + "@internxt/sdk": "=1.11.25", "@internxt/ui": "0.1.1", "@phosphor-icons/react": "^2.1.7", "@popperjs/core": "^2.11.6", "@reduxjs/toolkit": "^1.6.0", + "@serenity-kit/opaque": "^1.0.0", "@stripe/react-stripe-js": "^2.7.1", "@stripe/stripe-js": "^3.5.0", "@typeform/embed-react": "^1.19.0", @@ -36,6 +37,7 @@ "i18next": "^22.4.9", "i18next-browser-languagedetector": "^7.2.0", "idb": "^6.1.5", + "internxt-crypto": "https://github.com/internxt/crypto/releases/download/v.0.0.10-alpha/internxt-crypto-0.0.10-alpha.tgz", "js-file-download": "^0.4.12", "lint-staged": "^13.1.0", "lodash": "^4.17.21", diff --git a/src/app/analytics/ga.service.test.ts b/src/app/analytics/ga.service.test.ts index 6968213d1a..6966472f8c 100644 --- a/src/app/analytics/ga.service.test.ts +++ b/src/app/analytics/ga.service.test.ts @@ -513,4 +513,4 @@ describe('Testing GA Service', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/src/app/analytics/ga.service.ts b/src/app/analytics/ga.service.ts index 859af6679e..9870b33012 100644 --- a/src/app/analytics/ga.service.ts +++ b/src/app/analytics/ga.service.ts @@ -138,7 +138,7 @@ function trackBeginCheckout(params: TrackBeginCheckoutParams): void { } } - function trackPurchase(): void { +function trackPurchase(): void { try { const userSettings = localStorageService.getUser() as UserSettings; if (!userSettings) { diff --git a/src/services/auth.constants.ts b/src/services/auth.constants.ts new file mode 100644 index 0000000000..f39db72272 --- /dev/null +++ b/src/services/auth.constants.ts @@ -0,0 +1,4 @@ +export const ECC_KEY_AUX = 'user-private-key'; +export const KYBER_KEY_AUX = 'user-private-kyber-key'; +export const MNEMONIC_AUX = 'user-mnemonic'; +export const SESSION_KEY_AUX = 'User Session Key'; diff --git a/src/services/auth.crypto.test.ts b/src/services/auth.crypto.test.ts new file mode 100644 index 0000000000..97a3d804ad --- /dev/null +++ b/src/services/auth.crypto.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { generateUserSecrets, encryptUserKeysAndMnemonic, decryptUserKeysAndMnemonic } from './auth.crypto'; + +describe('Test auth crypto functions', () => { + it('should sucessfully encrypt and decrypt user keys and mnemonic', async () => { + const { keys, mnemonic } = await generateUserSecrets(); + const exportKey = 'Srp6AzybbyludWuaVwGoHa1C2H0Qtv7JR0sKGLSWe8Ho8_q9hezfYD2RYb9IUrW999pH4VlABgDLse484zAapg'; + + const { encMnemonic, encKeys } = await encryptUserKeysAndMnemonic(keys, mnemonic, exportKey); + + const { keys: decKeys, mnemonic: decMnemonic } = await decryptUserKeysAndMnemonic(encMnemonic, encKeys, exportKey); + + expect(keys).toStrictEqual(decKeys); + expect(mnemonic).toEqual(decMnemonic); + }); +}); diff --git a/src/services/auth.crypto.ts b/src/services/auth.crypto.ts new file mode 100644 index 0000000000..e7382dfbf0 --- /dev/null +++ b/src/services/auth.crypto.ts @@ -0,0 +1,115 @@ +import { + encryptSymmetrically, + deriveSymmetricCryptoKey, + decryptSymmetrically, + importSymmetricCryptoKey, +} from 'internxt-crypto/symmetric-crypto'; +import { getKeyFromPasswordAndSalt, getKeyFromPassword } from 'internxt-crypto/derive-key'; +import { + UTF8ToUint8, + base64ToUint8Array, + uint8ArrayToBase64, + genMnemonic, + mnemonicToBytes, + bytesToMnemonic, + uint8ToUTF8, +} from 'internxt-crypto/utils'; +import { UserKeys } from '@internxt/sdk'; +import { generateNewKeys } from 'app/crypto/services/pgp.service'; +import { ECC_KEY_AUX, KYBER_KEY_AUX, MNEMONIC_AUX, SESSION_KEY_AUX } from './auth.constants'; + +export async function encryptUserKeysAndMnemonic( + userKeys: UserKeys, + mnemonic: string, + exportKey: string, +): Promise<{ encMnemonic: string; encKeys: UserKeys }> { + const exportKeyBytes = safeBase64ToBytes(exportKey); + const cryptoKey = await deriveSymmetricCryptoKey(exportKeyBytes); + const key = UTF8ToUint8(userKeys.ecc.privateKey); + const encPrivateKey = await encryptSymmetrically(cryptoKey, key, UTF8ToUint8(ECC_KEY_AUX)); + const keyKyber = base64ToUint8Array(userKeys.kyber.privateKey); + const encPrivateKyberKey = await encryptSymmetrically(cryptoKey, keyKyber, UTF8ToUint8(KYBER_KEY_AUX)); + const mnemonicArray = mnemonicToBytes(mnemonic); + const mnemonicCipher = await encryptSymmetrically(cryptoKey, mnemonicArray, UTF8ToUint8(MNEMONIC_AUX)); + const encMnemonic = uint8ArrayToBase64(mnemonicCipher); + + const encKeys: UserKeys = { + ecc: { + privateKey: uint8ArrayToBase64(encPrivateKey), + publicKey: userKeys.ecc.publicKey, + }, + kyber: { + privateKey: uint8ArrayToBase64(encPrivateKyberKey), + publicKey: userKeys.kyber.publicKey, + }, + }; + return { encMnemonic, encKeys }; +} + +export async function decryptUserKeysAndMnemonic( + encMnemonic: string, + encKeys: UserKeys, + exportKey: string, +): Promise<{ keys: UserKeys; mnemonic: string }> { + const exportKeyBytes = safeBase64ToBytes(exportKey); + const cryptoKey = await deriveSymmetricCryptoKey(exportKeyBytes); + const encKey = base64ToUint8Array(encKeys.ecc.privateKey); + const privateKey = await decryptSymmetrically(cryptoKey, encKey, UTF8ToUint8(ECC_KEY_AUX)); + const encKyberKey = base64ToUint8Array(encKeys.kyber.privateKey); + const privateKyberKey = await decryptSymmetrically(cryptoKey, encKyberKey, UTF8ToUint8(KYBER_KEY_AUX)); + const encMnemonicArray = base64ToUint8Array(encMnemonic); + const mnemonicArray = await decryptSymmetrically(cryptoKey, encMnemonicArray, UTF8ToUint8(MNEMONIC_AUX)); + const mnemonic = bytesToMnemonic(mnemonicArray); + + const keys: UserKeys = { + ecc: { + privateKey: uint8ToUTF8(privateKey), + publicKey: encKeys.ecc.publicKey, + }, + kyber: { + privateKey: uint8ArrayToBase64(privateKyberKey), + publicKey: encKeys.kyber.publicKey, + }, + }; + return { keys, mnemonic }; +} + +export const encryptSessionKey = async ( + password: string, + sessionKey: string, +): Promise<{ sessionKeyEnc: string; salt: string }> => { + const { key, salt } = await getKeyFromPassword(password); + const cryptoKey = await importSymmetricCryptoKey(key); + const sessionKeyArray = safeBase64ToBytes(sessionKey); + const sessionKeyEncCipher = await encryptSymmetrically(cryptoKey, sessionKeyArray, UTF8ToUint8(SESSION_KEY_AUX)); + const sessionKeyEnc = uint8ArrayToBase64(sessionKeyEncCipher); + return { sessionKeyEnc, salt: uint8ArrayToBase64(salt) }; +}; + +export const safeBase64ToBytes = (urlSafeBase64: string): Uint8Array => { + const base64 = urlSafeBase64.replaceAll('-', '+').replaceAll('_', '/'); + const padding = (4 - (base64.length % 4)) % 4; + return base64ToUint8Array(base64 + '='.repeat(padding)); +}; + +export const decryptSessionKey = async (password: string, sessionKeyEnc: string, salt: string): Promise => { + const keyBytes = await getKeyFromPasswordAndSalt(password, base64ToUint8Array(salt)); + const key = await importSymmetricCryptoKey(keyBytes); + const sessionKeyCipher = base64ToUint8Array(sessionKeyEnc); + const sessionKeyArray = await decryptSymmetrically(key, sessionKeyCipher, UTF8ToUint8(SESSION_KEY_AUX)); + return sessionKeyArray; +}; + +export const generateUserSecrets = async (): Promise<{ keys: UserKeys; mnemonic: string }> => { + const mnemonic = genMnemonic(256); + + const { privateKeyArmored, publicKeyArmored, publicKyberKeyBase64, privateKyberKeyBase64 } = await generateNewKeys(); + const keys: UserKeys = { + ecc: { privateKey: privateKeyArmored, publicKey: publicKeyArmored }, + kyber: { + privateKey: privateKyberKeyBase64, + publicKey: publicKyberKeyBase64, + }, + }; + return { keys, mnemonic }; +}; diff --git a/src/services/auth.opaque.test.ts b/src/services/auth.opaque.test.ts new file mode 100644 index 0000000000..0c153578d7 --- /dev/null +++ b/src/services/auth.opaque.test.ts @@ -0,0 +1,363 @@ +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { server } from '@serenity-kit/opaque'; +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { SdkFactory } from 'app/core/factory/sdk'; +import * as authOpaqueService from './auth.opaque'; +import { RegisterOpaqueDetails, UserKeys } from '@internxt/sdk'; +import { decryptUserKeysAndMnemonic, safeBase64ToBytes } from './auth.crypto'; +import localStorageService from 'services/local-storage.service'; +import { computeMac } from 'internxt-crypto/hash'; +import { base64ToUint8Array, generateID, uuidToBytes } from 'internxt-crypto/utils'; + +function getMockUser(registerDetails: RegisterOpaqueDetails) { + const mockUser: UserSettings = { + uuid: 'mock-uuid', + email: registerDetails.email, + privateKey: registerDetails.keys.ecc.privateKey, + mnemonic: registerDetails.mnemonic, + userId: 'mock-userId', + name: registerDetails.name, + lastname: registerDetails.lastname, + username: 'mock-username', + bridgeUser: 'mock-bridgeUser', + bucket: 'mock-bucket', + backupsBucket: null, + root_folder_id: 0, + rootFolderId: 'mock-rootFolderId', + rootFolderUuid: undefined, + sharedWorkspace: false, + credit: 0, + publicKey: registerDetails.keys.ecc.publicKey, + revocationKey: '', + keys: registerDetails.keys, + appSumoDetails: null, + registerCompleted: false, + hasReferralsProgram: false, + createdAt: new Date(), + avatar: null, + emailVerified: false, + }; + + return mockUser; +} + +describe('logIn', () => { + const mockTwoFactorCode = '123456'; + const mockEmail = 'test-email'; + const mockPassword = 'password123'; + const mockCaptcha = 'captcha'; + const mockReferrer = 'referrer'; + const mockReferral = 'referral'; + + beforeAll(() => { + const serverSetup = server.createSetup(); + const registrationRecords = new Map(); + const users = new Map(); + const logedInUsers = new Map(); + const serverLoginStates = new Map(); + + const mockLocalStorage = (() => { + let store: Record = {}; + + return { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + removeItem: vi.fn((key: string) => { + delete store[key]; + }), + clear: vi.fn(() => { + store = {}; + }), + }; + })(); + + Object.defineProperty(window, 'localStorage', { + value: mockLocalStorage, + writable: true, + }); + + mockLocalStorage.clear(); + + vi.spyOn(SdkFactory, 'getNewApiInstance').mockReturnValue({ + createAuthClient: vi.fn().mockImplementation(() => { + return { + registerOpaqueStart: vi.fn().mockImplementation((email: string, registrationRequest: string) => { + const { registrationResponse } = server.createRegistrationResponse({ + serverSetup, + userIdentifier: email, + registrationRequest, + }); + + return { signUpResponse: registrationResponse }; + }), + + registerOpaqueFinish: vi + .fn() + .mockImplementation( + (registrationRecord: string, registerDetails: RegisterOpaqueDetails, startLoginRequest: string) => { + const email = registerDetails.email; + registrationRecords.set(email, registrationRecord); + + const { loginResponse, serverLoginState } = server.startLogin({ + userIdentifier: email, + registrationRecord, + serverSetup, + startLoginRequest, + }); + const user = getMockUser(registerDetails); + users.set(email, user); + + serverLoginStates.set(email, serverLoginState); + + return { loginResponse }; + }, + ), + + loginOpaqueStart: vi.fn().mockImplementation((email: string, startLoginRequest: string) => { + const { loginResponse, serverLoginState } = server.startLogin({ + userIdentifier: email, + registrationRecord: registrationRecords.get(email), + serverSetup, + startLoginRequest, + }); + + serverLoginStates.set(email, serverLoginState); + + return { loginResponse }; + }), + + loginOpaqueFinish: vi + .fn() + .mockImplementation((email: string, finishLoginRequest: string, twoFactorCode: string) => { + if (twoFactorCode && twoFactorCode !== mockTwoFactorCode) { + throw new Error(`Two factor code is incorrect, got ${twoFactorCode}, expected ${mockTwoFactorCode}`); + } + const { sessionKey } = server.finishLogin({ + finishLoginRequest, + serverLoginState: serverLoginStates.get(email) ?? '', + }); + + const user = users.get(email); + if (!user) { + throw new Error('User is not found'); + } + const sessionID = generateID(); + + logedInUsers.set(sessionID, { sessionKey, email }); + + return { user, sessionID }; + }), + disableTwoFactorAuth: vi + .fn() + .mockImplementation(async (mac: string, twoFactorCode: string, sessionID: string) => { + if (twoFactorCode !== mockTwoFactorCode) { + throw new Error('Two factor code is incorrect'); + } + + const record = logedInUsers.get(sessionID); + if (!record?.sessionKey) { + throw new Error('Session ID is incorrect'); + } + + const sessionKey = record.sessionKey; + const sessionKeyBytes = safeBase64ToBytes(sessionKey); + + const correctMac = computeMac(sessionKeyBytes, [Buffer.from(twoFactorCode), uuidToBytes(sessionID)]); + if (correctMac === mac) { + return true; + } else throw new Error('HMAC is incorrect'); + }), + }; + }), + createUsersClient: vi.fn().mockImplementation(() => { + return { + changePwdOpaqueStart: vi + .fn() + .mockImplementation(async (mac: string, sessionID: string, registrationRequest: string) => { + const record = logedInUsers.get(sessionID); + if (!record?.sessionKey) { + throw new Error('Session ID is incorrect'); + } + + const sessionKey = record.sessionKey; + const sessionKeyBytes = safeBase64ToBytes(sessionKey); + const correctMac = computeMac(sessionKeyBytes, [ + safeBase64ToBytes(registrationRequest), + uuidToBytes(sessionID), + ]); + if (correctMac !== mac) { + throw new Error('HMAC is incorrect'); + } + const email = record.email ?? ''; + + const { registrationResponse } = server.createRegistrationResponse({ + serverSetup, + userIdentifier: email, + registrationRequest, + }); + + return { registrationResponse }; + }), + changePwdOpaqueFinish: vi + .fn() + .mockImplementation( + async ( + mac: string, + sessionID: string, + registrationRecord: string, + encMnemonic: string, + encKeys: UserKeys, + startLoginRequest: string, + ) => { + const record = logedInUsers.get(sessionID); + if (!record?.sessionKey) { + throw new Error('Session ID is incorrect'); + } + + const sessionKey = record.sessionKey; + + const sessionKeyBytes = safeBase64ToBytes(sessionKey); + const correctMac = computeMac(sessionKeyBytes, [ + uuidToBytes(sessionID), + safeBase64ToBytes(registrationRecord), + base64ToUint8Array(encMnemonic), + base64ToUint8Array(encKeys.ecc.privateKey), + base64ToUint8Array(encKeys.ecc.publicKey), + base64ToUint8Array(encKeys.kyber.privateKey), + base64ToUint8Array(encKeys.kyber.publicKey), + safeBase64ToBytes(startLoginRequest), + ]); + if (correctMac !== mac) { + throw new Error('HMAC is incorrect'); + } + const email = record.email ?? ''; + + if (correctMac !== mac) { + throw new Error('HMAC is incorrect'); + } + + const oldUser = users.get(email); + if (!oldUser) { + throw new Error('No user found'); + } + const newUser = { + ...oldUser, + encKeys, + encMnemonic, + }; + + users.set(email, newUser); + registrationRecords.set(email, registrationRecord); + + const { loginResponse, serverLoginState } = server.startLogin({ + userIdentifier: email, + registrationRecord, + serverSetup, + startLoginRequest, + }); + + serverLoginStates.set(email, serverLoginState); + + return { loginResponse }; + }, + ), + }; + }), + } as any); + }); + it('should successfully sign up', async () => { + const { xUser, xNewToken, xToken, mnemonic } = await authOpaqueService.doSignUpOpaque( + mockEmail, + mockPassword, + mockCaptcha, + ); + expect(xUser).toBeDefined(); + expect(xNewToken).toBeDefined(); + expect(xToken).toBeDefined(); + expect(mnemonic).toBeDefined(); + }); + + it('should successfully log in', async () => { + const { user, newToken, token, mnemonic } = await authOpaqueService.doLoginOpaque( + mockEmail, + mockPassword, + mockTwoFactorCode, + ); + expect(user).toBeDefined(); + expect(newToken).toBeDefined(); + expect(token).toBeDefined(); + expect(mnemonic).toBeDefined(); + }); + + let sessionKeyTest, sessionIdTest, exportKeyTest: string; + it('should successfully sign up and then log in', async () => { + const { sessionKey: sessionKeySignup, exportKey: exportKeySignUp } = await authOpaqueService.signupOpaque( + mockEmail, + mockPassword, + mockCaptcha, + mockReferrer, + mockReferral, + ); + const { + sessionKey: sessionKeyLogin, + exportKey: exportKeyLogin, + sessionID, + user, + } = await authOpaqueService.loginOpaque(mockEmail, mockPassword, mockTwoFactorCode); + + localStorageService.set('xNewToken', sessionID); + + const { keys, mnemonic } = await decryptUserKeysAndMnemonic(user.mnemonic, user.keys, exportKeyLogin); + + const clearUser = { + ...user, + mnemonic, + privateKey: keys.ecc.privateKey, + keys, + }; + + localStorageService.set('xUser', JSON.stringify(clearUser)); + sessionKeyTest = sessionKeyLogin; + exportKeyTest = exportKeyLogin; + sessionIdTest = sessionID; + + await authOpaqueService.setSessionKey(mockPassword, sessionKeyLogin); + + expect(sessionKeyLogin).not.toEqual(sessionKeySignup); + expect(exportKeyLogin).toEqual(exportKeySignUp); + }); + + it('should not log in with a wrong passwor or 2FA code', async () => { + await expect(authOpaqueService.loginOpaque(mockEmail, 'wrong pwd', mockTwoFactorCode)).rejects.toThrow( + 'Opaque login failed', + ); + + await expect(authOpaqueService.loginOpaque(mockEmail, mockPassword, 'wrong 2FA code')).rejects.toThrow( + 'Two factor code is incorrect', + ); + }); + + it('should change the password and then successfully log in with the new password', async () => { + const mockNewPassword = 'newPassword123'; + const { + exportKey: exportKeyPwdChange, + sessionID: sessionIdPwdChange, + sessionKey: sessionKeyPwdChange, + } = await authOpaqueService.doChangePasswordOpaque(mockNewPassword, mockPassword, sessionIdTest); + const { + sessionKey: sessionKeyNewLogin, + exportKey: exportKeyNewLogin, + sessionID: newSessionID, + } = await authOpaqueService.loginOpaque(mockEmail, mockNewPassword, mockTwoFactorCode); + + expect(sessionKeyTest).not.toEqual(sessionKeyNewLogin); + expect(newSessionID).not.toEqual(sessionIdTest); + expect(exportKeyNewLogin).not.toEqual(exportKeyTest); + + expect(sessionKeyTest).not.toEqual(sessionKeyPwdChange); + expect(newSessionID).not.toEqual(sessionIdPwdChange); + expect(exportKeyNewLogin).toEqual(exportKeyPwdChange); + }); +}); diff --git a/src/services/auth.opaque.ts b/src/services/auth.opaque.ts new file mode 100644 index 0000000000..0d5d1e1e6e --- /dev/null +++ b/src/services/auth.opaque.ts @@ -0,0 +1,235 @@ +import { client } from '@serenity-kit/opaque'; +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { SdkFactory } from 'app/core/factory/sdk'; +import localStorageService from 'services/local-storage.service'; +import { computeMac } from 'internxt-crypto/hash'; + +import { RegisterOpaqueDetails } from '@internxt/sdk'; +import { readReferalCookie } from './auth.service'; +import { + decryptUserKeysAndMnemonic, + encryptUserKeysAndMnemonic, + encryptSessionKey, + decryptSessionKey, + generateUserSecrets, + safeBase64ToBytes, +} from './auth.crypto'; +import { base64ToUint8Array, uuidToBytes } from 'internxt-crypto/utils'; + +import { ProfileInfoOpaque, OpaqueLoginError } from './auth.opaque.types'; + +export const loginOpaque = async ( + email: string, + password: string, + twoFactorCode: string, +): Promise => { + const authClient = SdkFactory.getNewApiInstance().createAuthClient(); + const { clientLoginState, startLoginRequest } = client.startLogin({ + password, + }); + + const { loginResponse } = await authClient.loginOpaqueStart(email, startLoginRequest); + const { user, sessionID, sessionKey, exportKey, token } = await finishOpaqueLogin( + clientLoginState, + loginResponse, + password, + email, + twoFactorCode, + ); + + return { user, sessionID, sessionKey, exportKey, token }; +}; + +export const doLoginOpaque = async ( + email: string, + password: string, + twoFactorCode: string, +): Promise<{ token: string; user: UserSettings; mnemonic: string; newToken: string }> => { + const { + sessionID, + user: loggedUser, + sessionKey, + exportKey, + token, + } = await loginOpaque(email, password, twoFactorCode); + + const { keys, mnemonic } = await decryptUserKeysAndMnemonic(loggedUser.mnemonic, loggedUser.keys, exportKey); + + const user = { + ...loggedUser, + mnemonic, + privateKey: keys.ecc.privateKey, + keys, + }; + + localStorageService.set('xMnemonic', mnemonic); + localStorageService.set('xNewToken', token); + localStorageService.set('sessionID', sessionID); + await setSessionKey(password, sessionKey); + + return { token: sessionID, user, mnemonic, newToken: sessionID }; +}; + +export const signupOpaque = async ( + email: string, + password: string, + captcha: string, + referrer: string, + referral: string, +) => { + const { clientRegistrationState, registrationRequest } = client.startRegistration({ password }); + const authClient = SdkFactory.getNewApiInstance().createAuthClient(); + const { signUpResponse } = await authClient.registerOpaqueStart(email, registrationRequest); + + const { exportKey, registrationRecord } = client.finishRegistration({ + clientRegistrationState, + registrationResponse: signUpResponse, + password, + }); + const { keys, mnemonic } = await generateUserSecrets(); + const { encKeys, encMnemonic } = await encryptUserKeysAndMnemonic(keys, mnemonic, exportKey); + + const registerDetails: RegisterOpaqueDetails = { + name: 'My', + lastname: 'Internxt', + email: email.toLowerCase(), + keys: encKeys, + mnemonic: encMnemonic, + referral, + captcha, + referrer, + }; + + const { clientLoginState, startLoginRequest } = client.startLogin({ + password, + }); + + const { loginResponse } = await authClient.registerOpaqueFinish( + registrationRecord, + registerDetails, + startLoginRequest, + ); + + const { user, sessionID, sessionKey, token } = await finishOpaqueLogin( + clientLoginState, + loginResponse, + password, + email, + '', + ); + + return { user, sessionID, sessionKey, mnemonic, exportKey, token }; +}; + +export const doSignUpOpaque = async (email: string, password: string, captcha: string) => { + const referrer = ''; + const referral = readReferalCookie() ?? ''; + + const { + user: xUser, + sessionID, + sessionKey, + mnemonic, + } = await signupOpaque(email, password, captcha, referrer, referral); + + await setSessionKey(password, sessionKey); + + return { xToken: sessionID, xUser, mnemonic, xNewToken: sessionID }; +}; + +export const setSessionKey = async (password: string, sessionKey: string): Promise => { + const { sessionKeyEnc, salt } = await encryptSessionKey(password, sessionKey); + localStorageService.setSessionKey(sessionKeyEnc, salt); +}; + +export const getSessionKey = async (password: string): Promise => { + const sessionKeyEnc = localStorageService.getSessionKey() || ''; + const salt = localStorageService.getSessionKeySalt() || ''; + return decryptSessionKey(password, sessionKeyEnc, salt); +}; + +export const getMac = async (password: string, request: Uint8Array[]): Promise => { + const sessionKey = await getSessionKey(password); + return computeMac(sessionKey, request); +}; + +const finishOpaqueLogin = async ( + clientLoginState: string, + loginResponse: string, + password: string, + email: string, + twoFactorCode: string, +) => { + const loginResult = client.finishLogin({ + clientLoginState, + loginResponse, + password, + }); + if (!loginResult) { + throw new OpaqueLoginError(); + } + const { finishLoginRequest, sessionKey, exportKey } = loginResult; + const authClient = SdkFactory.getNewApiInstance().createAuthClient(); + const { sessionID, user, token } = await authClient.loginOpaqueFinish(email, finishLoginRequest, twoFactorCode); + + return { exportKey, sessionID, sessionKey, user, token }; +}; + +export const doChangePasswordOpaque = async (newPassword: string, currentPassword: string, sessionID: string) => { + const { clientRegistrationState, registrationRequest } = client.startRegistration({ password: newPassword }); + + const mac = await getMac(currentPassword, [safeBase64ToBytes(registrationRequest), uuidToBytes(sessionID)]); + + const usersClient = SdkFactory.getNewApiInstance().createUsersClient(); + + const { registrationResponse } = await usersClient.changePwdOpaqueStart(mac, sessionID, registrationRequest); + const { exportKey, registrationRecord: newRegistrationRecord } = client.finishRegistration({ + clientRegistrationState, + registrationResponse, + password: newPassword, + }); + + const user = localStorageService.getUser() as UserSettings; + const { encKeys, encMnemonic } = await encryptUserKeysAndMnemonic(user.keys, user.mnemonic, exportKey); + + const { clientLoginState, startLoginRequest } = client.startLogin({ + password: newPassword, + }); + + const macNew = await getMac(currentPassword, [ + uuidToBytes(sessionID), + safeBase64ToBytes(newRegistrationRecord), + base64ToUint8Array(encMnemonic), + base64ToUint8Array(encKeys.ecc.privateKey), + base64ToUint8Array(encKeys.ecc.publicKey), + base64ToUint8Array(encKeys.kyber.privateKey), + base64ToUint8Array(encKeys.kyber.publicKey), + safeBase64ToBytes(startLoginRequest), + ]); + + const { loginResponse } = await usersClient.changePwdOpaqueFinish( + macNew, + sessionID, + newRegistrationRecord, + encMnemonic, + encKeys, + startLoginRequest, + ); + + const { sessionID: newSessionID, sessionKey: newSessionKey } = await finishOpaqueLogin( + clientLoginState, + loginResponse, + newPassword, + user.email, + '', + ); + + return { exportKey, sessionID: newSessionID, sessionKey: newSessionKey }; +}; + +export const changePasswordOpaque = async (newPassword: string, currentPassword: string): Promise => { + const currentSessionID = localStorageService.get('xNewToken') || ''; + const { sessionID, sessionKey } = await doChangePasswordOpaque(newPassword, currentPassword, currentSessionID); + await setSessionKey(newPassword, sessionKey); + localStorageService.set('xNewToken', sessionID); +}; diff --git a/src/services/auth.opaque.types.ts b/src/services/auth.opaque.types.ts new file mode 100644 index 0000000000..8167ba9438 --- /dev/null +++ b/src/services/auth.opaque.types.ts @@ -0,0 +1,16 @@ +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; + +export type ProfileInfoOpaque = { + user: UserSettings; + sessionID: string; + sessionKey: string; + exportKey: string; + token: string; +}; + +export class OpaqueLoginError extends Error { + public constructor() { + super('Opaque login failed'); + Object.setPrototypeOf(this, OpaqueLoginError.prototype); + } +} diff --git a/src/services/auth.service.test.ts b/src/services/auth.service.test.ts index 610bbb5b61..978319edeb 100644 --- a/src/services/auth.service.test.ts +++ b/src/services/auth.service.test.ts @@ -967,13 +967,13 @@ describe('Security and validation', () => { createAuthClient: vi.fn().mockReturnValue(mockAuthClient), } as any); - expect(await authService.is2FANeeded('test@example.com')).toBe(true); + expect((await authService.is2FAorOpaqueNeeded('test@example.com')).tfaEnabled).toBe(true); mockAuthClient.securityDetails.mockResolvedValue({ tfaEnabled: false }); - expect(await authService.is2FANeeded('test@example.com')).toBe(false); + expect((await authService.is2FAorOpaqueNeeded('test@example.com')).tfaEnabled).toBe(false); mockAuthClient.securityDetails.mockRejectedValue({ message: 'User not found', status: 404 }); - await expect(authService.is2FANeeded('test@example.com')).rejects.toThrow('User not found'); + await expect(authService.is2FAorOpaqueNeeded('test@example.com')).rejects.toThrow('User not found'); }); }); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 30a6ef37f0..fc17852946 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -43,6 +43,7 @@ import { generateMnemonic, validateMnemonic } from 'bip39'; import { SdkFactory } from 'app/core/factory/sdk'; import errorService from 'services/error.service'; import vpnAuthService from './vpnAuth.service'; +import { doLoginOpaque, doSignUpOpaque } from './auth.opaque'; type ProfileInfo = { user: UserSettings; @@ -69,6 +70,7 @@ export type SignUpParams = { token: string; redeemCodeObject: boolean; dispatch: AppDispatch; + useOpaqueLogin?: boolean; }; type LogInParams = { @@ -77,6 +79,7 @@ type LogInParams = { twoFactorCode: string; dispatch: AppDispatch; loginType?: 'web' | 'desktop'; + useOpaqueLogin?: boolean; }; export type AuthenticateUserParams = { @@ -89,6 +92,7 @@ export type AuthenticateUserParams = { token?: string; redeemCodeObject?: boolean; doSignUp?: RegisterFunction; + useOpaqueLogin?: boolean; }; const getCurrentUrlParams = (): Record => { @@ -135,13 +139,13 @@ export function cancelAccount(): Promise { return authClient.sendUserDeactivationEmail(token); } -export const is2FANeeded = async (email: string): Promise => { +export const is2FAorOpaqueNeeded = async (email: string): Promise<{ tfaEnabled: boolean; useOpaqueLogin: boolean }> => { const authClient = SdkFactory.getNewApiInstance().createAuthClient(); const securityDetails = await authClient.securityDetails(email).catch((error) => { throw new AppError(error.message ?? 'Login error', error.status ?? 500); }); - return securityDetails.tfaEnabled; + return { tfaEnabled: securityDetails.tfaEnabled, useOpaqueLogin: securityDetails.useOpaqueLogin }; }; const getAuthClient = (authType: 'web' | 'desktop') => { @@ -551,8 +555,10 @@ export const unblockAccount = (token: string): Promise => { }; export const signUp = async (params: SignUpParams) => { - const { doSignUp, email, password, token, redeemCodeObject, dispatch } = params; - const { xUser, xToken, xNewToken, mnemonic } = await doSignUp(email, password, token); + const { doSignUp, email, password, token, redeemCodeObject, dispatch, useOpaqueLogin = false } = params; + const { xUser, xToken, xNewToken, mnemonic } = await (useOpaqueLogin + ? doSignUpOpaque(email, password, token) + : doSignUp(email, password, token)); localStorageService.clear(); @@ -590,8 +596,10 @@ export const signUp = async (params: SignUpParams) => { }; export const logIn = async (params: LogInParams): Promise => { - const { email, password, twoFactorCode, dispatch, loginType = 'web' } = params; - const { token, newToken, user, mnemonic } = await doLogin(email, password, twoFactorCode, loginType); + const { email, password, twoFactorCode, dispatch, loginType = 'web', useOpaqueLogin = false } = params; + const { token, newToken, user, mnemonic } = await (useOpaqueLogin + ? doLoginOpaque(email, password, twoFactorCode) + : doLogin(email, password, twoFactorCode, loginType)); dispatch(userActions.setUser(user)); try { @@ -623,13 +631,14 @@ export const authenticateUser = async (params: AuthenticateUserParams): Promise< token = '', redeemCodeObject = false, doSignUp, + useOpaqueLogin = false, } = params; if (authMethod === 'signIn') { - const profileInfo = await logIn({ email, password, twoFactorCode, dispatch, loginType }); + const profileInfo = await logIn({ email, password, twoFactorCode, dispatch, loginType, useOpaqueLogin }); globalThis.gtag('event', 'User Signin', { method: 'email' }); return profileInfo; } else if (authMethod === 'signUp' && doSignUp) { - const profileInfo = await signUp({ doSignUp, email, password, token, redeemCodeObject, dispatch }); + const profileInfo = await signUp({ doSignUp, email, password, token, redeemCodeObject, dispatch, useOpaqueLogin }); return profileInfo; } else { throw new Error(`Unknown authMethod: ${authMethod}`); @@ -638,7 +647,7 @@ export const authenticateUser = async (params: AuthenticateUserParams): Promise< const authService = { logOut, - check2FANeeded: is2FANeeded, + check2FANeeded: is2FAorOpaqueNeeded, readReferalCookie, cancelAccount, store2FA, diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index b8ac0ce535..95599a3059 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -66,14 +66,32 @@ function clear(): void { localStorage.removeItem('theme:isDark'); localStorage.removeItem('xInvitedToken'); localStorage.removeItem('xResourcesToken'); + localStorage.removeItem('sessionKey'); + localStorage.removeItem('sessionKeySalt'); localStorage.removeItem(STORAGE_KEYS.B2B_WORKSPACE); localStorage.removeItem(STORAGE_KEYS.WORKSPACE_CREDENTIALS); localStorage.removeItem(STORAGE_KEYS.GCLID); } +function getSessionKey(): string | null { + return localStorage.getItem('sessionKey'); +} + +function getSessionKeySalt(): string | null { + return localStorage.getItem('sessionKeySalt'); +} + +function setSessionKey(sessionKey: string, salt: string): void { + localStorage.setItem('sessionKey', sessionKey); + localStorage.setItem('sessionKeySalt', salt); +} + const localStorageService = { set, get, + getSessionKey, + getSessionKeySalt, + setSessionKey, getUser, getWorkspace, hasCompletedTutorial, diff --git a/src/services/opaque.test.ts b/src/services/opaque.test.ts new file mode 100644 index 0000000000..34c5f58bc0 --- /dev/null +++ b/src/services/opaque.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import * as opaque from '@serenity-kit/opaque'; +import { encryptUserKeysAndMnemonic, generateUserSecrets } from './auth.crypto'; + +describe('Opaque registration record geenration', () => { + const password = 'test123.'; + const email = 'testbusiness05@inxt.com'; + + const serverSetup = + 'awirun08Dxx3yBpGdd0W2-j4Tl5ip02M5Uu7EVRhtqUzEEdW5EhlP1QC1z3UX8hB7cavoCyem4Kl0iCymdTsbk_tbiJu8-zzrWF3S1nQ2cGY5TkDXIatNKh5riaw7xINwkTOycgxvsIENsPn2W19OgAw2_Zih_1f4Px6ncj7-iw'; + + it('should generate registration record', async () => { + const { clientRegistrationState, registrationRequest } = opaque.client.startRegistration({ password }); + const { registrationResponse } = opaque.server.createRegistrationResponse({ + serverSetup, + userIdentifier: email, + registrationRequest, + }); + const { registrationRecord } = opaque.client.finishRegistration({ + clientRegistrationState, + registrationResponse, + password, + }); + console.log('Registration record', registrationRecord); + expect(registrationRecord).toBeDefined(); + }); + it('should encrypt keys', async () => { + const registrationRecord = + 'jIbcZt2Zb4N8NuIZngBA7qphxYxdFCxxI-8fxIWEoymmbwcQRv2ywpR8sCR2UTxrzQFOyb-dhXcmFXQB4TQEurIaij2sRYNaTc08uaP60PuXAbMvjCXMmSUe3VuO_Krgpaz6ZvqvwCuoPumIhSJqk47YhbVnJ924kqSjzFjR5CwiZhYqLw8dTFvni83FiATYNw3isf3ezrqksx23IlxatOWjU9ob4DnXtTLW3luy9brspfHzlqhTWZZY6Qc2VIlg'; + const { clientLoginState, startLoginRequest } = opaque.client.startLogin({ + password, + }); + const { loginResponse, serverLoginState } = opaque.server.startLogin({ + userIdentifier: email, + registrationRecord, + serverSetup, + startLoginRequest, + }); + const loginResult = opaque.client.finishLogin({ + clientLoginState, + loginResponse, + password, + }); + if (!loginResult) { + throw new Error('no login results'); + } + const { exportKey, finishLoginRequest, sessionKey } = loginResult; + + const { sessionKey: sessionKeyServer } = opaque.server.finishLogin({ + finishLoginRequest, + serverLoginState, + }); + expect(sessionKey).toBe(sessionKeyServer); + const { keys, mnemonic } = await generateUserSecrets(); + + const { encKeys, encMnemonic } = await encryptUserKeysAndMnemonic(keys, mnemonic, exportKey); + console.log('Encrypted keys', encKeys); + console.log('Encrypted mnemonic', encMnemonic); + const expectedKey = 'dnFsRmZwILu2tNDqVdJll6KYBDQziZvTWuw7uX8JSmZLveqQtuU1UO9qs5toXMakNsqmDHBJlnOlq52FG78IXw'; + expect(encKeys).toBeDefined(); + expect(encMnemonic).toBeDefined(); + expect(exportKey).toBe(expectedKey); + }); +}); diff --git a/src/views/Login/components/LogIn.tsx b/src/views/Login/components/LogIn.tsx index 768f46ee16..44f27d04d8 100644 --- a/src/views/Login/components/LogIn.tsx +++ b/src/views/Login/components/LogIn.tsx @@ -11,7 +11,7 @@ import { twoFactorRegexPattern } from 'services/validation.service'; import { RootState } from 'app/store'; import { useAppDispatch } from 'app/store/hooks'; import { userActions } from 'app/store/slices/user'; -import authService, { authenticateUser, is2FANeeded } from 'services/auth.service'; +import authService, { authenticateUser, is2FAorOpaqueNeeded } from 'services/auth.service'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { Button } from '@internxt/ui'; @@ -181,7 +181,7 @@ export default function LogIn(): JSX.Element { const { email, password } = formData; try { - const isTfaEnabled = await is2FANeeded(email); + const { tfaEnabled: isTfaEnabled, useOpaqueLogin } = await is2FAorOpaqueNeeded(email); if (!isTfaEnabled || showTwoFactor) { const loginType: 'desktop' | 'web' = isUniversalLinkMode ? 'desktop' : 'web'; @@ -192,6 +192,7 @@ export default function LogIn(): JSX.Element { twoFactorCode, dispatch, loginType, + useOpaqueLogin, }; const { token, user, mnemonic } = await authenticateUser(authParams); diff --git a/tsconfig.json b/tsconfig.json index 9ffaf5260e..36f15715d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/yarn.lock b/yarn.lock index 3030ef5a52..f01187e605 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1397,6 +1397,11 @@ resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b" integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== +"@emailjs/browser@^4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@emailjs/browser/-/browser-4.4.1.tgz#ad5684af5a912c0ab415202184845eb3270c4c81" + integrity sha512-DGSlP9sPvyFba3to2A50kDtZ+pXVp/0rhmqs2LmbMS3I5J8FSOgLwzY2Xb4qfKlOVHh29EAutLYwe5yuEZmEFg== + "@emotion/cache@^10.0.27": version "10.0.29" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" @@ -1906,10 +1911,10 @@ version "1.0.2" resolved "https://codeload.github.com/internxt/prettier-config/tar.gz/9fa74e9a2805e1538b50c3809324f1c9d0f3e4f9" -"@internxt/sdk@=1.11.17": - version "1.11.17" - resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.11.17.tgz#2f5bdada5d3cbf5cfc685a21c24b5df3ff51d8c8" - integrity sha512-91iEUvZizlwX6KBEFJ3JdFiGrhMBQ9R54sTc3Pei9QtV2FYTU8nTVEPYAg39tLOGzT/kVuplYOtBxfk6wFtSDA== +"@internxt/sdk@=1.11.25": + version "1.11.25" + resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.11.25.tgz#9d39c0fac9d86fbc2aa1fbd1e6342b987b10daa3" + integrity sha512-2uS4gQh8T3vKXX1Nzpr41Z4r/AqF8+IJZVHqa+2YfOFWxJ5/yc0JCrDJqI/mUTBa27spPZNua2ZgcwyuUeZEgQ== dependencies: axios "1.13.2" uuid "11.1.0" @@ -2040,11 +2045,31 @@ outvariant "^1.4.3" strict-event-emitter "^0.5.1" +"@noble/curves@~2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-2.0.1.tgz#64ba8bd5e8564a02942655602515646df1cdb3ad" + integrity sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw== + dependencies: + "@noble/hashes" "2.0.1" + +"@noble/hashes@2.0.1", "@noble/hashes@^2.0.1", "@noble/hashes@~2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e" + integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== + "@noble/hashes@^1.2.0": version "1.3.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/post-quantum@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@noble/post-quantum/-/post-quantum-0.5.2.tgz#51fdd58a97e3dae2cbe2349d2af2b10fcd8f226c" + integrity sha512-etMDBkCuB95Xj/gfsWYBD2x+84IjL4uMLd/FhGoUUG/g+eh0K2eP7pJz1EmvpN8Df3vKdoWVAc7RxIBCHQfFHQ== + dependencies: + "@noble/curves" "~2.0.0" + "@noble/hashes" "~2.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2996,6 +3021,24 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz#848f99b0d9936d92221bb6070baeff4db6947a30" integrity sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw== +"@scure/base@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-2.0.0.tgz#ba6371fddf92c2727e88ad6ab485db6e624f9a98" + integrity sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w== + +"@scure/bip39@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-2.0.1.tgz#47a6dc15e04faf200041239d46ae3bb7c3c96add" + integrity sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg== + dependencies: + "@noble/hashes" "2.0.1" + "@scure/base" "2.0.0" + +"@serenity-kit/opaque@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@serenity-kit/opaque/-/opaque-1.0.0.tgz#ef265667cd13977a53f22732f0d204452b5fe59c" + integrity sha512-UCu92RBFroWaUOdrIJzjXW8M/wqGGi7/2J8D5BPbCNAYU27iyzr5JDZ0N6wq31IhNhEIr7C/8V3+9aSgXi4F3A== + "@socket.io/component-emitter@~3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" @@ -3968,7 +4011,7 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@1.13.2: +axios@1.13.2, axios@^1.11.0: version "1.13.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687" integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA== @@ -5756,6 +5799,11 @@ flatted@^3.3.1: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== +flexsearch@^0.8.205: + version "0.8.212" + resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.212.tgz#b9509af778a991b938292e36fe0809a4ece4b940" + integrity sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw== + follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" @@ -6127,7 +6175,7 @@ hash-base@~3.0, hash-base@~3.0.4: inherits "^2.0.4" safe-buffer "^5.2.1" -hash-wasm@^4.11.0: +hash-wasm@^4.11.0, hash-wasm@^4.12.0: version "4.12.0" resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.12.0.tgz#f9f1a9f9121e027a9acbf6db5d59452ace1ef9bb" integrity sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ== @@ -6217,6 +6265,11 @@ husky@^7.0.2: resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== +husky@^9.1.7: + version "9.1.7" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" + integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== + i18next-browser-languagedetector@^7.2.0: version "7.2.1" resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz#1968196d437b4c8db847410c7c33554f6c448f6f" @@ -6236,6 +6289,11 @@ idb@^6.1.5: resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b" integrity sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw== +idb@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.3.tgz#c91e558f15a8d53f1d7f53a094d226fc3ad71fd9" + integrity sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg== + ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -6315,6 +6373,21 @@ ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +"internxt-crypto@https://github.com/internxt/crypto/releases/download/v.0.0.10-alpha/internxt-crypto-0.0.10-alpha.tgz": + version "0.0.10-alpha" + resolved "https://github.com/internxt/crypto/releases/download/v.0.0.10-alpha/internxt-crypto-0.0.10-alpha.tgz#d20cb24c44873c483f7dab498c365e1b1ffd17b1" + dependencies: + "@emailjs/browser" "^4.4.1" + "@noble/hashes" "^2.0.1" + "@noble/post-quantum" "^0.5.2" + "@scure/bip39" "^2.0.1" + axios "^1.11.0" + flexsearch "^0.8.205" + hash-wasm "^4.12.0" + husky "^9.1.7" + idb "^8.0.3" + uuid "^13.0.0" + invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -9560,6 +9633,11 @@ uuid@11.1.0, uuid@^11.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== +uuid@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8" + integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w== + uuid@^8.3.0: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"