diff --git a/src/crypto/keychainManager.ts b/src/crypto/keychainManager.ts index 9f8d140b..748941bd 100644 --- a/src/crypto/keychainManager.ts +++ b/src/crypto/keychainManager.ts @@ -15,19 +15,25 @@ import { perform2FAVerification } from '../middleware/perform2FAVerification'; const SERVICE = 'dashlane-cli'; -export const setLocalKey = (login: string, shouldNotSaveMasterPassword: boolean, localKey?: Buffer): Buffer => { +export const generateLocalKey = (): Buffer => { + const localKey = crypto.randomBytes(32); if (!localKey) { - localKey = crypto.randomBytes(32); - if (!localKey) { - throw new Error('Unable to generate AES local key'); - } + throw new Error('Unable to generate AES local key'); } + return localKey; +}; - if (!shouldNotSaveMasterPassword) { +export const setLocalKey = (login: string, localKey: Buffer, callbackOnError: (errorMessage: string) => void) => { + try { const entry = new Entry(SERVICE, login); entry.setPassword(localKey.toString('base64')); + } catch (error) { + let errorMessage = 'unknown error'; + if (error instanceof Error) { + errorMessage = error.message; + } + callbackOnError(errorMessage); } - return localKey; }; const getLocalKey = (login: string): Buffer | undefined => { @@ -77,19 +83,7 @@ const getSecretsWithoutDB = async ( login: string, shouldNotSaveMasterPassword: boolean ): Promise => { - let localKey: Buffer; - try { - localKey = setLocalKey(login, shouldNotSaveMasterPassword); - } catch (error) { - let errorMessage = 'unknown error'; - if (error instanceof Error) { - errorMessage = error.message; - } - winston.debug(`Unable to reach OS keychain: ${errorMessage}`); - throw new Error( - 'Your OS keychain is probably unreachable. Install it or disable its usage via `dcli configure save-master-password false`' - ); - } + const localKey = generateLocalKey(); // Register the user's device const { deviceAccessKey, deviceSecretKey, serverKey } = await registerDevice({ login }); @@ -115,6 +109,13 @@ const getSecretsWithoutDB = async ( const masterPasswordEncrypted = encryptAES(localKey, Buffer.from(masterPassword)); const localKeyEncrypted = encryptAES(derivate, localKey); + if (!shouldNotSaveMasterPassword) { + setLocalKey(login, localKey, (errorMessage) => { + warnUnreachableKeychainDisabled(errorMessage); + shouldNotSaveMasterPassword = true; + }); + } + db.prepare('REPLACE INTO device VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)') .bind( login, @@ -163,7 +164,12 @@ const getSecretsWithoutKeychain = async (login: string, deviceConfiguration: Dev await decrypt(deviceConfiguration.secretKeyEncrypted, { type: 'alreadyComputed', symmetricKey: localKey }) ).toString('hex'); - setLocalKey(login, deviceConfiguration.shouldNotSaveMasterPassword, localKey); + if (!deviceConfiguration.shouldNotSaveMasterPassword) { + setLocalKey(login, localKey, (errorMessage) => { + winston.warn(`Unable to reach OS keychain because of error: "${errorMessage}". \ +Install it or disable its usage via \`dcli configure save-master-password false\`.`); + }); + } return { login, @@ -270,3 +276,9 @@ export const getSecrets = async ( secretKey, }; }; + +export const warnUnreachableKeychainDisabled = (errorMessage: string) => { + winston.warn(`Unable to reach OS keychain because of error: "${errorMessage}", so its use has been disabled. \ +To retry using it, please execute \`dcli configure save-master-password true\`. \ +Until then, you will have to retype your master password each time you run the CLI.`); +}; diff --git a/src/middleware/configure.ts b/src/middleware/configure.ts index e74f2dc2..2a5c6449 100644 --- a/src/middleware/configure.ts +++ b/src/middleware/configure.ts @@ -1,7 +1,7 @@ import Database from 'better-sqlite3'; import winston from 'winston'; import { encryptAES } from '../crypto/encrypt'; -import { deleteLocalKey, setLocalKey } from '../crypto/keychainManager'; +import { deleteLocalKey, setLocalKey, warnUnreachableKeychainDisabled } from '../crypto/keychainManager'; import { Secrets } from '../types'; interface ConfigureSaveMasterPassword { @@ -17,7 +17,8 @@ interface ConfigureDisableAutoSync { } export const configureSaveMasterPassword = (params: ConfigureSaveMasterPassword) => { - const { db, secrets, shouldNotSaveMasterPassword } = params; + const { db, secrets } = params; + let shouldNotSaveMasterPassword = params.shouldNotSaveMasterPassword; if (shouldNotSaveMasterPassword) { // Forget the local key stored in the OS keychain because the master password and the DB are enough to retrieve the @@ -30,7 +31,7 @@ export const configureSaveMasterPassword = (params: ConfigureSaveMasterPassword) if (error instanceof Error) { errorMessage = error.message; } - winston.debug(`Unable to delete the local key from the keychain: ${errorMessage}`); + winston.warn(`Unable to delete the local key from the keychain: ${errorMessage}`); } } @@ -41,8 +42,13 @@ export const configureSaveMasterPassword = (params: ConfigureSaveMasterPassword) // Set encrypted master password in the DB masterPasswordEncrypted = encryptAES(secrets.localKey, Buffer.from(secrets.masterPassword)); - // Set local key in the OS keychain - setLocalKey(secrets.login, shouldNotSaveMasterPassword, secrets.localKey); + if (!shouldNotSaveMasterPassword) { + // Set local key in the OS keychain + setLocalKey(secrets.login, secrets.localKey, (errorMessage: string) => { + warnUnreachableKeychainDisabled(errorMessage); + shouldNotSaveMasterPassword = true; + }); + } } db.prepare('UPDATE device SET masterPasswordEncrypted = ?, shouldNotSaveMasterPassword = ? WHERE login = ?')