From dea97107d9d701f181daf4363b4ec126b0766964 Mon Sep 17 00:00:00 2001 From: Corentin Mors Date: Wed, 16 Aug 2023 15:44:25 +0200 Subject: [PATCH] Add support for SSO login authentication (#168) Relates to #103 --- package.json | 1 + src/command-handlers/configure.ts | 4 +- src/command-handlers/sync.ts | 4 +- .../getAuthenticationMethodsForDevice.ts | 2 +- src/endpoints/performSSOVerification.ts | 22 +++++++ src/modules/auth/registerDevice.ts | 43 ++++++++----- src/modules/auth/sso/index.ts | 63 +++++++++++++++++++ src/modules/auth/sso/types.ts | 13 ++++ src/modules/auth/sso/utils.ts | 25 ++++++++ src/modules/crypto/buildSsoRemoteKey.ts | 44 +++++++++++++ src/modules/crypto/decrypt.ts | 40 ++++++++++-- src/modules/crypto/encrypt.ts | 2 +- .../crypto/encryptedDataDeserialization.ts | 2 +- src/modules/crypto/index.ts | 1 + src/modules/crypto/keychainManager.ts | 62 ++++++++++++------ src/modules/crypto/test.ts | 6 +- src/modules/crypto/types.ts | 6 ++ src/modules/crypto/xor.ts | 11 ++++ src/types.ts | 3 +- src/utils/dialogs.ts | 19 +++--- yarn.lock | 21 +++++++ 21 files changed, 335 insertions(+), 59 deletions(-) create mode 100644 src/endpoints/performSSOVerification.ts create mode 100644 src/modules/auth/sso/index.ts create mode 100644 src/modules/auth/sso/types.ts create mode 100644 src/modules/auth/sso/utils.ts create mode 100644 src/modules/crypto/buildSsoRemoteKey.ts create mode 100644 src/modules/crypto/xor.ts diff --git a/package.json b/package.json index 559d5434..5f424302 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "inquirer-search-list": "^1.2.6", "jsonpath-plus": "^7.2.0", "otplib": "^12.0.1", + "playwright": "^1.37.0", "winston": "^3.10.0", "xml-js": "^1.6.11", "zlib": "^1.0.5" diff --git a/src/command-handlers/configure.ts b/src/command-handlers/configure.ts index 6eaa56dc..f9ccfe94 100644 --- a/src/command-handlers/configure.ts +++ b/src/command-handlers/configure.ts @@ -1,5 +1,5 @@ import winston from 'winston'; -import { encryptAES } from '../modules/crypto/encrypt'; +import { encryptAesCbcHmac256 } from '../modules/crypto/encrypt'; import { deleteLocalKey, setLocalKey, warnUnreachableKeychainDisabled } from '../modules/crypto/keychainManager'; import { connectAndPrepare } from '../modules/database'; import { parseBooleanString } from '../utils'; @@ -31,7 +31,7 @@ export const configureSaveMasterPassword = async (boolean: string) => { masterPasswordEncrypted = null; } else { // Set encrypted master password in the DB - masterPasswordEncrypted = encryptAES(secrets.localKey, Buffer.from(secrets.masterPassword)); + masterPasswordEncrypted = encryptAesCbcHmac256(secrets.localKey, Buffer.from(secrets.masterPassword)); if (!shouldNotSaveMasterPassword) { // Set local key in the OS keychain diff --git a/src/command-handlers/sync.ts b/src/command-handlers/sync.ts index 9d4d8ed6..62b64c82 100644 --- a/src/command-handlers/sync.ts +++ b/src/command-handlers/sync.ts @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import winston from 'winston'; import { connectAndPrepare } from '../modules/database'; import { decrypt } from '../modules/crypto/decrypt'; -import { encryptAES } from '../modules/crypto/encrypt'; +import { encryptAesCbcHmac256 } from '../modules/crypto/encrypt'; import { replaceMasterPassword } from '../modules/crypto/keychainManager'; import { getLatestContent } from '../endpoints'; import type { DeviceConfiguration, Secrets } from '../types'; @@ -73,7 +73,7 @@ export const sync = async (params: Sync) => { } return null; } - const encryptedTransactionContent = encryptAES(secrets.localKey, transactionContent); + const encryptedTransactionContent = encryptAesCbcHmac256(secrets.localKey, transactionContent); return [ secrets.login, transac.identifier, diff --git a/src/endpoints/getAuthenticationMethodsForDevice.ts b/src/endpoints/getAuthenticationMethodsForDevice.ts index d53fcefb..efdeadb1 100644 --- a/src/endpoints/getAuthenticationMethodsForDevice.ts +++ b/src/endpoints/getAuthenticationMethodsForDevice.ts @@ -13,7 +13,7 @@ interface GetAuthenticationMethodsForDeviceParams { supportedMethods?: SupportedAuthenticationMethod[]; } -interface GetAuthenticationMethodsForDeviceResult { +export interface GetAuthenticationMethodsForDeviceResult { /** The authentication methods available for the user */ verifications: ( | { diff --git a/src/endpoints/performSSOVerification.ts b/src/endpoints/performSSOVerification.ts new file mode 100644 index 00000000..98673e01 --- /dev/null +++ b/src/endpoints/performSSOVerification.ts @@ -0,0 +1,22 @@ +import { requestAppApi } from '../requestApi'; + +interface PerformSsoVerificationPayload { + /** The login of the user */ + login: string; + /** The SSO token */ + ssoToken: string; +} + +export interface PerformSsoVerificationBodyData { + /** Authentication ticket usable several time */ + authTicket: string; +} + +export const performSSOVerification = (params: PerformSsoVerificationPayload) => + requestAppApi({ + path: 'authentication/PerformSsoVerification', + payload: { + login: params.login, + ssoToken: params.ssoToken, + }, + }); diff --git a/src/modules/auth/registerDevice.ts b/src/modules/auth/registerDevice.ts index 33147428..0019c4f0 100644 --- a/src/modules/auth/registerDevice.ts +++ b/src/modules/auth/registerDevice.ts @@ -1,6 +1,6 @@ import winston from 'winston'; +import { doSSOVerification } from './sso'; import { - CompleteDeviceRegistrationWithAuthTicketOutput, completeDeviceRegistration, performDashlaneAuthenticatorVerification, performDuoPushVerification, @@ -10,16 +10,13 @@ import { import { askOtp, askToken, askVerificationMethod } from '../../utils'; import { getAuthenticationMethodsForDevice } from '../../endpoints/getAuthenticationMethodsForDevice'; import { requestEmailTokenVerification } from '../../endpoints/requestEmailTokenVerification'; -import type { SupportedAuthenticationMethod } from '../../types'; interface RegisterDevice { login: string; deviceName: string; } -export const registerDevice = async ( - params: RegisterDevice -): Promise => { +export const registerDevice = async (params: RegisterDevice) => { const { login, deviceName } = params; winston.debug('Registering the device...'); @@ -30,25 +27,32 @@ export const registerDevice = async ( throw new Error('Master password-less is currently not supported'); } + const nonEmptyVerifications = verifications.filter((method) => method.type); + const selectedVerificationMethod = - verifications.length > 1 - ? await askVerificationMethod(verifications.map((method) => method.type as SupportedAuthenticationMethod)) - : verifications[0].type; + nonEmptyVerifications.length > 1 + ? await askVerificationMethod(nonEmptyVerifications) + : nonEmptyVerifications[0]; let authTicket: string; - if (selectedVerificationMethod === 'duo_push') { + let ssoSpKey: string | null = null; + if (!selectedVerificationMethod || Object.keys(selectedVerificationMethod).length === 0) { + throw new Error('No verification method selected'); + } + + if (selectedVerificationMethod.type === 'duo_push') { winston.info('Please accept the Duo push notification on your phone'); ({ authTicket } = await performDuoPushVerification({ login })); - } else if (selectedVerificationMethod === 'dashlane_authenticator') { + } else if (selectedVerificationMethod.type === 'dashlane_authenticator') { winston.info('Please accept the Dashlane Authenticator push notification on your phone'); ({ authTicket } = await performDashlaneAuthenticatorVerification({ login })); - } else if (selectedVerificationMethod === 'totp') { + } else if (selectedVerificationMethod.type === 'totp') { const otp = await askOtp(); ({ authTicket } = await performTotpVerification({ login, otp, })); - } else if (selectedVerificationMethod === 'email_token') { + } else if (selectedVerificationMethod.type === 'email_token') { await requestEmailTokenVerification({ login }); const token = await askToken(); @@ -56,10 +60,21 @@ export const registerDevice = async ( login, token, })); + } else if (selectedVerificationMethod.type === 'sso') { + if (selectedVerificationMethod.ssoInfo.isNitroProvider) { + throw new Error('Confidential SSO is currently not supported'); + } + + ({ authTicket, ssoSpKey } = await doSSOVerification({ + requestedLogin: login, + serviceProviderURL: selectedVerificationMethod.ssoInfo.serviceProviderUrl, + })); } else { - throw new Error('Auth verification method not supported: ' + verifications[0].type); + throw new Error('Auth verification method not supported: ' + selectedVerificationMethod.type); } // Complete the device registration and save the result - return completeDeviceRegistration({ login, deviceName, authTicket }); + const completeDeviceRegistrationResponse = await completeDeviceRegistration({ login, deviceName, authTicket }); + + return { ...completeDeviceRegistrationResponse, ssoSpKey }; }; diff --git a/src/modules/auth/sso/index.ts b/src/modules/auth/sso/index.ts new file mode 100644 index 00000000..0f6884e2 --- /dev/null +++ b/src/modules/auth/sso/index.ts @@ -0,0 +1,63 @@ +import { chromium } from 'playwright'; +import { DASHLANE_APP_REGEX, extractSsoInfoFromUrl } from './utils'; +import { performSSOVerification } from '../../../endpoints/performSSOVerification'; + +interface SSOParams { + requestedLogin: string; + serviceProviderURL: string; +} + +const openIdPAndWaitForRedirectURL = async (serviceProviderURL: string, userLogin: string): Promise => { + return new Promise((resolve, reject) => { + void (async () => { + try { + const browser = await chromium.launch({ headless: false, channel: 'chrome' }); + const context = await browser.newContext(); + const page = await context.newPage(); + + page.on('framenavigated', (frame) => { + const url = page.url(); + if (frame === page.mainFrame() && url.match(DASHLANE_APP_REGEX)) { + void browser.close(); + resolve(new URL(url)); + } + }); + + browser.on('disconnected', () => { + reject(new Error('Browser closed before SSO login')); + }); + + await page.goto(serviceProviderURL); + + // attempt to fill the login field + await page + .getByLabel('email') + .or(page.getByLabel('Username')) + .fill(userLogin) + .catch(() => null); + } catch (error) { + reject(error); + } + })(); + }); +}; + +export const doSSOVerification = async ({ requestedLogin, serviceProviderURL }: SSOParams) => { + const redirectURL = await openIdPAndWaitForRedirectURL(serviceProviderURL, requestedLogin); + const ssoInfo = extractSsoInfoFromUrl(redirectURL); + + if (requestedLogin !== ssoInfo.login) { + throw new Error('Login received from IdP does not match'); + } + + if (ssoInfo.currentAuths !== ssoInfo.expectedAuths) { + throw new Error('SSO Migration is not supported'); + } + + const ssoVerificationResult = await performSSOVerification({ + login: ssoInfo.login, + ssoToken: ssoInfo.ssoToken, + }); + + return { ...ssoVerificationResult, ssoSpKey: ssoInfo.key }; +}; diff --git a/src/modules/auth/sso/types.ts b/src/modules/auth/sso/types.ts new file mode 100644 index 00000000..f16590ed --- /dev/null +++ b/src/modules/auth/sso/types.ts @@ -0,0 +1,13 @@ +export enum SsoMigrationServerMethod { + SSO = 'sso', + MP = 'master_password', +} + +export interface SSOAuthenticationInfo { + login: string; + ssoToken: string; + key: string; + exists: string; + currentAuths: SsoMigrationServerMethod; + expectedAuths: SsoMigrationServerMethod; +} diff --git a/src/modules/auth/sso/utils.ts b/src/modules/auth/sso/utils.ts new file mode 100644 index 00000000..b2cbda7a --- /dev/null +++ b/src/modules/auth/sso/utils.ts @@ -0,0 +1,25 @@ +import { SSOAuthenticationInfo } from './types'; + +export const DASHLANE_APP_REGEX = /https:\/\/app.dashlane.com/; + +/** + * Convert a URL into SSOAuthenticationInfo structure + * @param url Redirection URL provided by the IdP with required data for SSO login + * @returns SSOAuthenticationInfo data structure + */ +export const extractSsoInfoFromUrl = (url: URL): SSOAuthenticationInfo => { + // Data is available in URL hash, within the form of search parameters. + const search_params = new URLSearchParams(url.hash.substring(1)); + + // To be rewritten properly + return Object.fromEntries( + Object.entries({ + login: '', + ssoToken: '', + key: '', + exists: '', + currentAuths: '', + expectedAuths: '', + }).map(([k]) => [k, search_params.get(k)]) + ) as unknown as SSOAuthenticationInfo; +}; diff --git a/src/modules/crypto/buildSsoRemoteKey.ts b/src/modules/crypto/buildSsoRemoteKey.ts new file mode 100644 index 00000000..8d5a57ae --- /dev/null +++ b/src/modules/crypto/buildSsoRemoteKey.ts @@ -0,0 +1,44 @@ +import { decryptAesCbcHmac256 } from './decrypt'; +import { deserializeEncryptedData } from './encryptedDataDeserialization'; +import { RemoteKey } from './types'; +import { xor } from './xor'; + +interface BuildSsoRemoteKey { + ssoServerKey: string | undefined; + ssoSpKey: string | undefined | null; + remoteKeys: RemoteKey[] | undefined; +} + +export const decryptSsoRemoteKey = (params: BuildSsoRemoteKey) => { + const { ssoServerKey, ssoSpKey, remoteKeys } = params; + + if (!ssoServerKey) { + throw new Error('SSO server key is missing'); + } + if (!ssoSpKey) { + throw new Error('SSO Service Provider key is missing'); + } + if (!remoteKeys || remoteKeys.length === 0) { + throw new Error('Remote keys are missing'); + } + + const remoteKey = remoteKeys.filter((key) => key.type === 'sso')[0]; + + if (!remoteKey) { + throw new Error('Remote SSO key is missing'); + } + + const ssoKeysXored = xor(Buffer.from(ssoServerKey, 'base64'), Buffer.from(ssoSpKey, 'base64')); + + const remoteKeyBase64 = Buffer.from(remoteKey.key, 'base64'); + const decodedBase64 = remoteKeyBase64.toString('ascii'); + const { encryptedData } = deserializeEncryptedData(decodedBase64, remoteKeyBase64); + + const decryptedRemoteKey = decryptAesCbcHmac256({ + cipherData: encryptedData.cipherData, + originalKey: ssoKeysXored, + inflatedKey: true, + }); + + return decryptedRemoteKey.toString('base64'); +}; diff --git a/src/modules/crypto/decrypt.ts b/src/modules/crypto/decrypt.ts index 940737cb..1c86b883 100644 --- a/src/modules/crypto/decrypt.ts +++ b/src/modules/crypto/decrypt.ts @@ -9,8 +9,31 @@ import { hmacSha256, sha512 } from './hash'; import { deserializeEncryptedData } from './encryptedDataDeserialization'; import { BackupEditTransaction, Secrets, SymmetricKeyGetter } from '../../types'; -const decryptCipherData = (cipherData: CipherData, originalKey: Buffer): Buffer => { - const combinedKey = sha512(originalKey); +interface DecryptAesCbcHmac256Params { + /** The cipher data to decrypt */ + cipherData: CipherData; + /** The original key used to encrypt the cipher data */ + originalKey: Buffer; + /** If `true`, the originalKey is already inflated (should be 64 bytes long) + * + * If `false`, the originalKey is inflated using sha512 + */ + inflatedKey?: boolean; +} + +export const decryptAesCbcHmac256 = (params: DecryptAesCbcHmac256Params): Buffer => { + const { cipherData, originalKey, inflatedKey } = params; + + let combinedKey: Buffer; + if (inflatedKey) { + if (originalKey.length !== 64) { + throw new Error(`crypto key must be 64 bytes long but is ${originalKey.length} bytes long`); + } + combinedKey = originalKey; + } else { + combinedKey = sha512(originalKey); + } + const cipheringKey = combinedKey.slice(0, 32); const macKey = combinedKey.slice(32); @@ -31,6 +54,7 @@ export const decrypt = async (encryptedAsBase64: string, symmetricKeyGetter: Sym const { encryptedData, derivationMethodBytes } = deserializeEncryptedData(decodedBase64, buffer); let symmetricKey: Buffer | undefined; + switch (symmetricKeyGetter.type) { case 'alreadyComputed': symmetricKey = symmetricKeyGetter.symmetricKey; @@ -49,7 +73,11 @@ export const decrypt = async (encryptedAsBase64: string, symmetricKeyGetter: Sym } } - return decryptCipherData(encryptedData.cipherData, symmetricKey); + return decryptAesCbcHmac256({ + cipherData: encryptedData.cipherData, + originalKey: symmetricKey, + inflatedKey: encryptedData.cipherConfig.cipherMode === 'cbchmac64', + }); }; export const decryptTransaction = async ( @@ -95,9 +123,9 @@ export const getDerivateUsingParametersFromEncryptedData = async ( 32, cipheringMethod.keyDerivation.hashMethod ); + case 'noderivation': + return Promise.resolve(Buffer.from(masterPassword, 'base64')); default: - throw new Error( - `Impossible to compute derivate with derivation method '${cipheringMethod.keyDerivation.algo}'` - ); + throw new Error('Impossible to compute derivate with derivation method'); } }; diff --git a/src/modules/crypto/encrypt.ts b/src/modules/crypto/encrypt.ts index 7b23172b..d60cab27 100644 --- a/src/modules/crypto/encrypt.ts +++ b/src/modules/crypto/encrypt.ts @@ -3,7 +3,7 @@ import { serializeEncryptedData } from './encryptedDataSerialization'; import { hmacSha256, sha512 } from './hash'; import { EncryptedData } from './types'; -export const encryptAES = (originalKey: Buffer, content: Buffer): string => { +export const encryptAesCbcHmac256 = (originalKey: Buffer, content: Buffer): string => { const combinedKey = sha512(originalKey); const cipheringKey = combinedKey.slice(0, 32); const macKey = combinedKey.slice(32); diff --git a/src/modules/crypto/encryptedDataDeserialization.ts b/src/modules/crypto/encryptedDataDeserialization.ts index b7c0c227..fd8d9f4f 100644 --- a/src/modules/crypto/encryptedDataDeserialization.ts +++ b/src/modules/crypto/encryptedDataDeserialization.ts @@ -128,7 +128,7 @@ const deserializeSymmetricCipherConfig = ( const mode = extractNextEncryptedDataStringComponent(encryptedDataString.substring(cursor)); cursor += mode.cursorAfter; - if (mode.component !== 'cbchmac') { + if (mode.component !== 'cbchmac' && mode.component !== 'cbchmac64') { throw new Error(`Unrecognized cipher mode: ${mode.component}`); } diff --git a/src/modules/crypto/index.ts b/src/modules/crypto/index.ts index ecddc9a3..e28803ff 100644 --- a/src/modules/crypto/index.ts +++ b/src/modules/crypto/index.ts @@ -1,2 +1,3 @@ export { decryptTransaction, decryptTransactions } from './decrypt'; export { getSecrets } from './keychainManager'; +export * from './xor'; diff --git a/src/modules/crypto/keychainManager.ts b/src/modules/crypto/keychainManager.ts index 8772c370..9e3d0b45 100644 --- a/src/modules/crypto/keychainManager.ts +++ b/src/modules/crypto/keychainManager.ts @@ -4,9 +4,10 @@ import winston from 'winston'; import os from 'os'; import crypto from 'crypto'; import { decrypt, getDerivateUsingParametersFromEncryptedData } from './decrypt'; -import { encryptAES } from './encrypt'; +import { encryptAesCbcHmac256 } from './encrypt'; import { sha512 } from './hash'; import { EncryptedData } from './types'; +import { decryptSsoRemoteKey } from './buildSsoRemoteKey'; import { CLI_VERSION, cliVersionToString } from '../../cliVersion'; import { perform2FAVerification, registerDevice } from '../auth'; import { DeviceConfiguration, Secrets } from '../../types'; @@ -88,11 +89,14 @@ const getSecretsWithoutDB = async ( // Register the user's device const deviceCredentials = getDeviceCredentials(); - const { deviceAccessKey, deviceSecretKey, serverKey } = deviceCredentials + const { deviceAccessKey, deviceSecretKey, serverKey, ssoServerKey, ssoSpKey, remoteKeys } = deviceCredentials ? { deviceAccessKey: deviceCredentials.accessKey, deviceSecretKey: deviceCredentials.secretKey, serverKey: undefined, + ssoServerKey: undefined, + ssoSpKey: undefined, + remoteKeys: [], } : await registerDevice({ login, @@ -102,13 +106,21 @@ const getSecretsWithoutDB = async ( // Get the authentication type (mainly to identify if the user is with OTP2) const { type } = await get2FAStatusUnauthenticated({ login }); - let masterPassword = await askMasterPassword(); - - // In case of OTP2 + let masterPassword = ''; let serverKeyEncrypted = null; - if (type === 'totp_login' && serverKey) { - serverKeyEncrypted = encryptAES(localKey, Buffer.from(serverKey)); - masterPassword = serverKey + masterPassword; + const isSSO = type === 'sso'; + + // In case of SSO + if (isSSO) { + masterPassword = decryptSsoRemoteKey({ ssoServerKey, ssoSpKey, remoteKeys }); + } else { + masterPassword = await askMasterPassword(); + + // In case of OTP2 + if (type === 'totp_login' && serverKey) { + serverKeyEncrypted = encryptAesCbcHmac256(localKey, Buffer.from(serverKey)); + masterPassword = serverKey + masterPassword; + } } const derivate = await getDerivateUsingParametersFromEncryptedData( @@ -116,11 +128,12 @@ const getSecretsWithoutDB = async ( getDerivationParametersForLocalKey(login) ); - const deviceSecretKeyEncrypted = encryptAES(localKey, Buffer.from(deviceSecretKey, 'hex')); - const masterPasswordEncrypted = encryptAES(localKey, Buffer.from(masterPassword)); - const localKeyEncrypted = encryptAES(derivate, localKey); + const deviceSecretKeyEncrypted = encryptAesCbcHmac256(localKey, Buffer.from(deviceSecretKey, 'hex')); + const masterPasswordEncrypted = encryptAesCbcHmac256(localKey, Buffer.from(masterPassword)); + const localKeyEncrypted = encryptAesCbcHmac256(derivate, localKey); - if (!shouldNotSaveMasterPassword) { + const shouldSaveMasterPassword = !shouldNotSaveMasterPassword || isSSO; + if (shouldSaveMasterPassword) { setLocalKey(login, localKey, (errorMessage) => { warnUnreachableKeychainDisabled(errorMessage); shouldNotSaveMasterPassword = true; @@ -133,8 +146,8 @@ const getSecretsWithoutDB = async ( cliVersionToString(CLI_VERSION), deviceAccessKey, deviceSecretKeyEncrypted, - shouldNotSaveMasterPassword ? null : masterPasswordEncrypted, - shouldNotSaveMasterPassword ? 1 : 0, + shouldSaveMasterPassword ? masterPasswordEncrypted : null, + shouldSaveMasterPassword ? 0 : 1, localKeyEncrypted, 1, type, @@ -146,6 +159,7 @@ const getSecretsWithoutDB = async ( login, masterPassword, shouldNotSaveMasterPassword, + isSSO, localKey, accessKey: deviceAccessKey, secretKey: deviceSecretKey, @@ -153,8 +167,13 @@ const getSecretsWithoutDB = async ( }; const getSecretsWithoutKeychain = async (login: string, deviceConfiguration: DeviceConfiguration): Promise => { - let serverKey; + if (deviceConfiguration.authenticationMode === 'sso') { + throw new Error('SSO is currently not supported without the keychain'); + } + + /** The master password is a utf-8 string and in case it comes from a remote key with no derivation encoded in base64 */ let masterPassword = ''; + let serverKey: string | undefined; if (deviceConfiguration.authenticationMode === 'totp_login') { serverKey = await perform2FAVerification({ login, deviceAccessKey: deviceConfiguration.accessKey }); masterPassword = serverKey ?? ''; @@ -186,6 +205,7 @@ Install it or disable its usage via \`dcli configure save-master-password false\ login, masterPassword, shouldNotSaveMasterPassword: deviceConfiguration.shouldNotSaveMasterPassword, + isSSO: false, localKey, accessKey: deviceConfiguration.accessKey, secretKey, @@ -197,6 +217,10 @@ export const replaceMasterPassword = async ( secrets: Secrets, deviceConfiguration: DeviceConfiguration | null ): Promise => { + if (deviceConfiguration && deviceConfiguration.authenticationMode === 'sso') { + throw new Error("You can't replace the master password of an SSO account"); + } + const { localKey, login, accessKey, secretKey, shouldNotSaveMasterPassword } = secrets; let newMasterPassword = ''; @@ -206,7 +230,7 @@ export const replaceMasterPassword = async ( if (deviceConfiguration && deviceConfiguration.authenticationMode === 'totp_login') { serverKey = await perform2FAVerification({ login, deviceAccessKey: deviceConfiguration.accessKey }); newMasterPassword = serverKey ?? ''; - serverKeyEncrypted = encryptAES(secrets.localKey, Buffer.from(serverKey ?? '')); + serverKeyEncrypted = encryptAesCbcHmac256(secrets.localKey, Buffer.from(serverKey ?? '')); } newMasterPassword += await askMasterPassword(); @@ -216,8 +240,8 @@ export const replaceMasterPassword = async ( getDerivationParametersForLocalKey(login) ); - const masterPasswordEncrypted = encryptAES(secrets.localKey, Buffer.from(newMasterPassword)); - const localKeyEncrypted = encryptAES(derivate, localKey); + const masterPasswordEncrypted = encryptAesCbcHmac256(secrets.localKey, Buffer.from(newMasterPassword)); + const localKeyEncrypted = encryptAesCbcHmac256(derivate, localKey); db.prepare( 'UPDATE device SET localKeyEncrypted = ?, masterPasswordEncrypted = ?, serverKeyEncrypted = ? WHERE login = ?' @@ -234,6 +258,7 @@ export const replaceMasterPassword = async ( login, masterPassword: newMasterPassword, shouldNotSaveMasterPassword, + isSSO: false, localKey, accessKey, secretKey, @@ -282,6 +307,7 @@ export const getSecrets = async ( login, masterPassword, shouldNotSaveMasterPassword: deviceConfiguration.shouldNotSaveMasterPassword, + isSSO: deviceConfiguration.authenticationMode === 'sso', localKey, accessKey: deviceConfiguration.accessKey, secretKey, diff --git a/src/modules/crypto/test.ts b/src/modules/crypto/test.ts index eb8fa9ba..0a2fbf72 100644 --- a/src/modules/crypto/test.ts +++ b/src/modules/crypto/test.ts @@ -1,14 +1,14 @@ import { expect } from 'chai'; import * as crypto from 'crypto'; import { decrypt } from './decrypt'; -import { encryptAES } from './encrypt'; +import { encryptAesCbcHmac256 } from './encrypt'; import { deserializeEncryptedData } from './encryptedDataDeserialization'; describe('Encrypt and decrypt using random symmetric key', () => { it('ciphering params parsed after encryption are correct', () => { const input = 'The input string I want to encrypt'; const key = crypto.randomBytes(32); - const encryptedInput = encryptAES(key, Buffer.from(input)); + const encryptedInput = encryptAesCbcHmac256(key, Buffer.from(input)); const buffer = Buffer.from(encryptedInput, 'base64'); const decodedBase64 = buffer.toString('ascii'); @@ -26,7 +26,7 @@ describe('Encrypt and decrypt using random symmetric key', () => { it('decryption of encryption should successfully return the input', async () => { const input = 'The input string I want to encrypt'; const key = crypto.randomBytes(32); - const encryptedInput = encryptAES(key, Buffer.from(input)); + const encryptedInput = encryptAesCbcHmac256(key, Buffer.from(input)); const decryptedInput = await decrypt(encryptedInput, { type: 'alreadyComputed', symmetricKey: key }); expect(input).to.equal(decryptedInput.toString()); }); diff --git a/src/modules/crypto/types.ts b/src/modules/crypto/types.ts index c7e9f734..5acf2d3a 100644 --- a/src/modules/crypto/types.ts +++ b/src/modules/crypto/types.ts @@ -64,3 +64,9 @@ export interface EncryptedData { cipherConfig: SymmetricCipherConfig; cipherData: CipherData; } + +export interface RemoteKey { + uuid: string; + key: string; + type: 'sso' | 'master_password'; +} diff --git a/src/modules/crypto/xor.ts b/src/modules/crypto/xor.ts new file mode 100644 index 00000000..fe0daead --- /dev/null +++ b/src/modules/crypto/xor.ts @@ -0,0 +1,11 @@ +/** XOR two keys together, expects buffers to be of the same length */ +export const xor = (leftKey: Buffer, rightKey: Buffer): Buffer => { + if (leftKey.length !== rightKey.length) { + throw new Error('Keys must be of the same length'); + } + const res = []; + for (let i = 0; i < 64; i++) { + res.push(leftKey[i] ^ rightKey[i]); + } + return Buffer.from(res); +}; diff --git a/src/types.ts b/src/types.ts index ca4030e3..0327ac44 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ export interface Secrets { login: string; masterPassword: string; shouldNotSaveMasterPassword: boolean; + isSSO: boolean; localKey: Buffer; accessKey: string; secretKey: string; @@ -214,7 +215,7 @@ export interface VaultSecrets { notes: VaultNote[]; } -export type SupportedAuthenticationMethod = 'email_token' | 'totp' | 'duo_push' | 'dashlane_authenticator'; +export type SupportedAuthenticationMethod = 'email_token' | 'totp' | 'duo_push' | 'dashlane_authenticator' | 'sso'; export interface ParsedPath { secretId?: string; diff --git a/src/utils/dialogs.ts b/src/utils/dialogs.ts index baf91e11..62771801 100644 --- a/src/utils/dialogs.ts +++ b/src/utils/dialogs.ts @@ -2,13 +2,8 @@ import inquirer from 'inquirer'; import inquirerSearchList from 'inquirer-search-list'; import { removeUnderscoresAndCapitalize } from './strings'; import { getDeviceCredentials } from './deviceCredentials'; -import { - PrintableVaultCredential, - PrintableVaultNote, - SupportedAuthenticationMethod, - VaultCredential, - VaultNote, -} from '../types'; +import { PrintableVaultCredential, PrintableVaultNote, VaultCredential, VaultNote } from '../types'; +import { GetAuthenticationMethodsForDeviceResult } from '../endpoints/getAuthenticationMethodsForDevice'; import PromptConstructor = inquirer.prompts.PromptConstructor; export const prompt = inquirer.createPromptModule({ output: process.stderr }); @@ -157,14 +152,18 @@ export const askToken = async () => { return response.token; }; -export const askVerificationMethod = async (verificationMethods: SupportedAuthenticationMethod[]) => { - const response = await inquirer.prompt<{ verificationMethod: string }>([ +export const askVerificationMethod = async ( + verificationMethods: GetAuthenticationMethodsForDeviceResult['verifications'] +) => { + const response = await inquirer.prompt<{ + verificationMethod: GetAuthenticationMethodsForDeviceResult['verifications'][0]; + }>([ { type: 'list', name: 'verificationMethod', message: 'What second factor method would you like to use?', choices: verificationMethods.map((method) => { - return { name: removeUnderscoresAndCapitalize(method), value: method }; + return { name: removeUnderscoresAndCapitalize(method.type), value: method }; }), }, ]); diff --git a/yarn.lock b/yarn.lock index 34a98f18..e7d7650b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -126,6 +126,7 @@ __metadata: mocha: ^10.2.0 otplib: ^12.0.1 pkg: ^5.8.1 + playwright: ^1.37.0 prettier: ^2.8.8 ts-node: ^10.9.1 typescript: ^4.9.5 @@ -4257,6 +4258,26 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.37.0": + version: 1.37.0 + resolution: "playwright-core@npm:1.37.0" + bin: + playwright-core: cli.js + checksum: be91857b1bb56890b3929ed8cd529f5f5abe4b748f709abc8dbe5b0ee4c0e158247c3958fbccec6a52394dafe4619bc0e69c93d1b129a71b31ab3697771e4c5a + languageName: node + linkType: hard + +"playwright@npm:^1.37.0": + version: 1.37.0 + resolution: "playwright@npm:1.37.0" + dependencies: + playwright-core: 1.37.0 + bin: + playwright: cli.js + checksum: f07d1d69e4a13db428cb45af0e2cd5697cb490c7139e399adfc5702b547f741b6411b6442a9c91c0b39f374e0cb466b742de0b15490872a771ab8bdc68e767c5 + languageName: node + linkType: hard + "prebuild-install@npm:7.1.1, prebuild-install@npm:^7.1.0": version: 7.1.1 resolution: "prebuild-install@npm:7.1.1"