From eb35a6d8651a9dbf50afe1d1a641cfb5884d306b Mon Sep 17 00:00:00 2001 From: Corentin Mors Date: Thu, 3 Aug 2023 17:22:02 +0200 Subject: [PATCH] Register non interactive device (#147) This is related to #130, in order to be able to register a new device credentials for the non-interactive env device. --- documentation/pages/personal/devices.mdx | 29 +++++++++++++ src/command-handlers/devices.ts | 45 ++++++++++++++++++++- src/commands/devices.ts | 9 ++++- src/endpoints/completeDeviceRegistration.ts | 4 +- src/modules/auth/registerDevice.ts | 5 ++- src/modules/crypto/keychainManager.ts | 6 ++- 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/documentation/pages/personal/devices.mdx b/documentation/pages/personal/devices.mdx index a6331c73..18e81c96 100644 --- a/documentation/pages/personal/devices.mdx +++ b/documentation/pages/personal/devices.mdx @@ -49,3 +49,32 @@ dcli devices remove --all If you remove the current CLI device, you will need to do a `dcli logout` in order to re-authenticate. + +## Register a new non-interactive device + +In case you want to access your vault in non-interactive environment like CIs or servers, you can register a new device with the `register` command. + +```sh copy +dcli devices register "my_server" +``` + +Note that you will be prompted to validate the registration with a second factor authentication. + +This will create a new device named `my_server` and will print the device credentials. +Save them in a safe place (like in a secure note), as you won't be able to retrieve them later. +Run the suggested commands on your target device (your server or CI) to set the device credentials as environment variables. + +```sh +export DASHLANE_DEVICE_ACCESS_KEY=bdd5[..redacted..]6eb +export DASHLANE_DEVICE_SECRET_KEY=99f7d9bd547c0[..redacted..]c93fa2118cdf7e3d0 +export DASHLANE_MASTER_PASSWORD= +``` + +Please, replace `` with your actual master password. + + + OTP at each login and SSO are not supported for non-interactive devices. We recommend creating a dedicated Dashlane + account for your non-interactive devices. + + +Once you've set the environment variables, you can use the CLI to retrieve passwords, otp and notes and no interactive prompts will be shown. diff --git a/src/command-handlers/devices.ts b/src/command-handlers/devices.ts index 951b78c9..b2120c85 100644 --- a/src/command-handlers/devices.ts +++ b/src/command-handlers/devices.ts @@ -1,17 +1,22 @@ +import winston from 'winston'; import { connectAndPrepare, reset } from '../modules/database'; import { deactivateDevices, listDevices, ListDevicesOutput } from '../endpoints'; import { askConfirmReset, epochTimestampToIso } from '../utils'; +import { registerDevice } from '../modules/auth'; +import { get2FAStatusUnauthenticated } from '../endpoints/get2FAStatusUnauthenticated'; type OutputDevice = ListDevicesOutput['devices'][number] & { isCurrentDevice: boolean; }; export async function listAllDevices(options: { json: boolean }) { - const { secrets, deviceConfiguration } = await connectAndPrepare({ autoSync: false }); + const { secrets, deviceConfiguration, db } = await connectAndPrepare({ autoSync: false }); if (!deviceConfiguration) { throw new Error('Require to be connected'); } const listDevicesResponse = await listDevices({ secrets, login: deviceConfiguration.login }); + db.close(); + const result: OutputDevice[] = listDevicesResponse.devices.map( (device) => { ...device, isCurrentDevice: device.deviceId === secrets.accessKey } ); @@ -89,3 +94,41 @@ export async function removeAllDevices(devices: string[] | null, options: { all: } db.close(); } + +export const registerNonInteractiveDevice = async (deviceName: string, options: { json: boolean }) => { + const { + secrets: { login }, + db, + } = await connectAndPrepare({ autoSync: false }); + + const { type } = await get2FAStatusUnauthenticated({ login }); + + if (type === 'totp_login') { + throw new Error("You can't register a non-interactive device when you have OTP at each login enabled."); + } + + if (type === 'sso') { + throw new Error("You can't register a non-interactive device when you are using SSO."); + } + + const { deviceAccessKey, deviceSecretKey } = await registerDevice({ + login, + deviceName: `Non-Interactive - ${deviceName}`, + }); + + if (options.json) { + console.log( + JSON.stringify({ + DASHLANE_DEVICE_ACCESS_KEY: deviceAccessKey, + DASHLANE_DEVICE_SECRET_KEY: deviceSecretKey, + }) + ); + } else { + winston.info('The device credentials have been generated, save and run the following commands to export them:'); + console.log(`export DASHLANE_DEVICE_ACCESS_KEY=${deviceAccessKey}`); + console.log(`export DASHLANE_DEVICE_SECRET_KEY=${deviceSecretKey}`); + console.log(`export DASHLANE_MASTER_PASSWORD=`); + } + + db.close(); +}; diff --git a/src/commands/devices.ts b/src/commands/devices.ts index a31a9e23..40b91764 100644 --- a/src/commands/devices.ts +++ b/src/commands/devices.ts @@ -1,5 +1,5 @@ import { Command } from 'commander'; -import { listAllDevices, removeAllDevices } from '../command-handlers'; +import { listAllDevices, registerNonInteractiveDevice, removeAllDevices } from '../command-handlers'; export const devicesCommands = (params: { program: Command }) => { const { program } = params; @@ -19,4 +19,11 @@ export const devicesCommands = (params: { program: Command }) => { .argument('[device ids...]', 'ids of the devices to remove') .description('De-registers a list of devices. De-registering the CLI will implies doing a "dcli logout"') .action(removeAllDevices); + + devicesGroup + .command('register') + .description('Registers a new device to be used in non-interactive mode') + .argument('', 'name of the device to register') + .option('--json', 'Output in JSON format') + .action(registerNonInteractiveDevice); }; diff --git a/src/endpoints/completeDeviceRegistration.ts b/src/endpoints/completeDeviceRegistration.ts index d4269c82..23827bd5 100644 --- a/src/endpoints/completeDeviceRegistration.ts +++ b/src/endpoints/completeDeviceRegistration.ts @@ -1,10 +1,10 @@ -import os from 'os'; import { CLI_VERSION, cliVersionToString } from '../cliVersion'; import { requestAppApi } from '../requestApi'; interface CompleteDeviceRegistration { login: string; authTicket: string; + deviceName: string; } export interface CompleteDeviceRegistrationWithAuthTicketOutput { @@ -64,7 +64,7 @@ export const completeDeviceRegistration = (params: CompleteDeviceRegistration) = path: 'authentication/CompleteDeviceRegistrationWithAuthTicket', payload: { device: { - deviceName: `${os.hostname()} - ${os.platform()}-${os.arch()}`, + deviceName: params.deviceName, appVersion: `${cliVersionToString(CLI_VERSION)}`, platform: 'server_cli', osCountry: 'en_US', diff --git a/src/modules/auth/registerDevice.ts b/src/modules/auth/registerDevice.ts index 4efdc1f4..33147428 100644 --- a/src/modules/auth/registerDevice.ts +++ b/src/modules/auth/registerDevice.ts @@ -14,12 +14,13 @@ import type { SupportedAuthenticationMethod } from '../../types'; interface RegisterDevice { login: string; + deviceName: string; } export const registerDevice = async ( params: RegisterDevice ): Promise => { - const { login } = params; + const { login, deviceName } = params; winston.debug('Registering the device...'); // Log in via a compatible verification method @@ -60,5 +61,5 @@ export const registerDevice = async ( } // Complete the device registration and save the result - return completeDeviceRegistration({ login, authTicket }); + return completeDeviceRegistration({ login, deviceName, authTicket }); }; diff --git a/src/modules/crypto/keychainManager.ts b/src/modules/crypto/keychainManager.ts index 6945a168..4134c830 100644 --- a/src/modules/crypto/keychainManager.ts +++ b/src/modules/crypto/keychainManager.ts @@ -1,6 +1,7 @@ import { Database } from 'better-sqlite3'; import { Entry } from '@napi-rs/keyring'; import winston from 'winston'; +import os from 'os'; import crypto from 'crypto'; import { decrypt, getDerivateUsingParametersFromEncryptedData } from './decrypt'; import { encryptAES } from './encrypt'; @@ -85,7 +86,10 @@ const getSecretsWithoutDB = async ( const localKey = generateLocalKey(); // Register the user's device - const { deviceAccessKey, deviceSecretKey, serverKey } = await registerDevice({ login }); + const { deviceAccessKey, deviceSecretKey, serverKey } = await registerDevice({ + login, + deviceName: `${os.hostname()} - ${os.platform()}-${os.arch()}`, + }); // Get the authentication type (mainly to identify if the user is with OTP2) const { type } = await get2FAStatusUnauthenticated({ login });