Skip to content

Commit

Permalink
Register non interactive device (#147)
Browse files Browse the repository at this point in the history
This is related to #130, in order to be able to register a new device
credentials for the non-interactive env device.
  • Loading branch information
Mikescops authored Aug 3, 2023
1 parent fe70163 commit eb35a6d
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 7 deletions.
29 changes: 29 additions & 0 deletions documentation/pages/personal/devices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,32 @@ dcli devices remove --all
<Callout type="info" emoji="ℹ️">
If you remove the current CLI device, you will need to do a `dcli logout` in order to re-authenticate.
</Callout>

## 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=<insert your master password here>
```

Please, replace `<insert your master password here>` with your actual master password.

<Callout type="warning" emoji="⚠️">
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.
</Callout>

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.
45 changes: 44 additions & 1 deletion src/command-handlers/devices.ts
Original file line number Diff line number Diff line change
@@ -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) => <OutputDevice>{ ...device, isCurrentDevice: device.deviceId === secrets.accessKey }
);
Expand Down Expand Up @@ -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=<insert your master password here>`);
}

db.close();
};
9 changes: 8 additions & 1 deletion src/commands/devices.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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('<device name>', 'name of the device to register')
.option('--json', 'Output in JSON format')
.action(registerNonInteractiveDevice);
};
4 changes: 2 additions & 2 deletions src/endpoints/completeDeviceRegistration.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions src/modules/auth/registerDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import type { SupportedAuthenticationMethod } from '../../types';

interface RegisterDevice {
login: string;
deviceName: string;
}

export const registerDevice = async (
params: RegisterDevice
): Promise<CompleteDeviceRegistrationWithAuthTicketOutput> => {
const { login } = params;
const { login, deviceName } = params;
winston.debug('Registering the device...');

// Log in via a compatible verification method
Expand Down Expand Up @@ -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 });
};
6 changes: 5 additions & 1 deletion src/modules/crypto/keychainManager.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 });
Expand Down

0 comments on commit eb35a6d

Please sign in to comment.