Skip to content

Commit eb35a6d

Browse files
author
Corentin Mors
authored
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.
1 parent fe70163 commit eb35a6d

File tree

6 files changed

+91
-7
lines changed

6 files changed

+91
-7
lines changed

documentation/pages/personal/devices.mdx

+29
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,32 @@ dcli devices remove --all
4949
<Callout type="info" emoji="ℹ️">
5050
If you remove the current CLI device, you will need to do a `dcli logout` in order to re-authenticate.
5151
</Callout>
52+
53+
## Register a new non-interactive device
54+
55+
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.
56+
57+
```sh copy
58+
dcli devices register "my_server"
59+
```
60+
61+
Note that you will be prompted to validate the registration with a second factor authentication.
62+
63+
This will create a new device named `my_server` and will print the device credentials.
64+
Save them in a safe place (like in a secure note), as you won't be able to retrieve them later.
65+
Run the suggested commands on your target device (your server or CI) to set the device credentials as environment variables.
66+
67+
```sh
68+
export DASHLANE_DEVICE_ACCESS_KEY=bdd5[..redacted..]6eb
69+
export DASHLANE_DEVICE_SECRET_KEY=99f7d9bd547c0[..redacted..]c93fa2118cdf7e3d0
70+
export DASHLANE_MASTER_PASSWORD=<insert your master password here>
71+
```
72+
73+
Please, replace `<insert your master password here>` with your actual master password.
74+
75+
<Callout type="warning" emoji="⚠️">
76+
OTP at each login and SSO are not supported for non-interactive devices. We recommend creating a dedicated Dashlane
77+
account for your non-interactive devices.
78+
</Callout>
79+
80+
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.

src/command-handlers/devices.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
import winston from 'winston';
12
import { connectAndPrepare, reset } from '../modules/database';
23
import { deactivateDevices, listDevices, ListDevicesOutput } from '../endpoints';
34
import { askConfirmReset, epochTimestampToIso } from '../utils';
5+
import { registerDevice } from '../modules/auth';
6+
import { get2FAStatusUnauthenticated } from '../endpoints/get2FAStatusUnauthenticated';
47

58
type OutputDevice = ListDevicesOutput['devices'][number] & {
69
isCurrentDevice: boolean;
710
};
811

912
export async function listAllDevices(options: { json: boolean }) {
10-
const { secrets, deviceConfiguration } = await connectAndPrepare({ autoSync: false });
13+
const { secrets, deviceConfiguration, db } = await connectAndPrepare({ autoSync: false });
1114
if (!deviceConfiguration) {
1215
throw new Error('Require to be connected');
1316
}
1417
const listDevicesResponse = await listDevices({ secrets, login: deviceConfiguration.login });
18+
db.close();
19+
1520
const result: OutputDevice[] = listDevicesResponse.devices.map(
1621
(device) => <OutputDevice>{ ...device, isCurrentDevice: device.deviceId === secrets.accessKey }
1722
);
@@ -89,3 +94,41 @@ export async function removeAllDevices(devices: string[] | null, options: { all:
8994
}
9095
db.close();
9196
}
97+
98+
export const registerNonInteractiveDevice = async (deviceName: string, options: { json: boolean }) => {
99+
const {
100+
secrets: { login },
101+
db,
102+
} = await connectAndPrepare({ autoSync: false });
103+
104+
const { type } = await get2FAStatusUnauthenticated({ login });
105+
106+
if (type === 'totp_login') {
107+
throw new Error("You can't register a non-interactive device when you have OTP at each login enabled.");
108+
}
109+
110+
if (type === 'sso') {
111+
throw new Error("You can't register a non-interactive device when you are using SSO.");
112+
}
113+
114+
const { deviceAccessKey, deviceSecretKey } = await registerDevice({
115+
login,
116+
deviceName: `Non-Interactive - ${deviceName}`,
117+
});
118+
119+
if (options.json) {
120+
console.log(
121+
JSON.stringify({
122+
DASHLANE_DEVICE_ACCESS_KEY: deviceAccessKey,
123+
DASHLANE_DEVICE_SECRET_KEY: deviceSecretKey,
124+
})
125+
);
126+
} else {
127+
winston.info('The device credentials have been generated, save and run the following commands to export them:');
128+
console.log(`export DASHLANE_DEVICE_ACCESS_KEY=${deviceAccessKey}`);
129+
console.log(`export DASHLANE_DEVICE_SECRET_KEY=${deviceSecretKey}`);
130+
console.log(`export DASHLANE_MASTER_PASSWORD=<insert your master password here>`);
131+
}
132+
133+
db.close();
134+
};

src/commands/devices.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Command } from 'commander';
2-
import { listAllDevices, removeAllDevices } from '../command-handlers';
2+
import { listAllDevices, registerNonInteractiveDevice, removeAllDevices } from '../command-handlers';
33

44
export const devicesCommands = (params: { program: Command }) => {
55
const { program } = params;
@@ -19,4 +19,11 @@ export const devicesCommands = (params: { program: Command }) => {
1919
.argument('[device ids...]', 'ids of the devices to remove')
2020
.description('De-registers a list of devices. De-registering the CLI will implies doing a "dcli logout"')
2121
.action(removeAllDevices);
22+
23+
devicesGroup
24+
.command('register')
25+
.description('Registers a new device to be used in non-interactive mode')
26+
.argument('<device name>', 'name of the device to register')
27+
.option('--json', 'Output in JSON format')
28+
.action(registerNonInteractiveDevice);
2229
};

src/endpoints/completeDeviceRegistration.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import os from 'os';
21
import { CLI_VERSION, cliVersionToString } from '../cliVersion';
32
import { requestAppApi } from '../requestApi';
43

54
interface CompleteDeviceRegistration {
65
login: string;
76
authTicket: string;
7+
deviceName: string;
88
}
99

1010
export interface CompleteDeviceRegistrationWithAuthTicketOutput {
@@ -64,7 +64,7 @@ export const completeDeviceRegistration = (params: CompleteDeviceRegistration) =
6464
path: 'authentication/CompleteDeviceRegistrationWithAuthTicket',
6565
payload: {
6666
device: {
67-
deviceName: `${os.hostname()} - ${os.platform()}-${os.arch()}`,
67+
deviceName: params.deviceName,
6868
appVersion: `${cliVersionToString(CLI_VERSION)}`,
6969
platform: 'server_cli',
7070
osCountry: 'en_US',

src/modules/auth/registerDevice.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import type { SupportedAuthenticationMethod } from '../../types';
1414

1515
interface RegisterDevice {
1616
login: string;
17+
deviceName: string;
1718
}
1819

1920
export const registerDevice = async (
2021
params: RegisterDevice
2122
): Promise<CompleteDeviceRegistrationWithAuthTicketOutput> => {
22-
const { login } = params;
23+
const { login, deviceName } = params;
2324
winston.debug('Registering the device...');
2425

2526
// Log in via a compatible verification method
@@ -60,5 +61,5 @@ export const registerDevice = async (
6061
}
6162

6263
// Complete the device registration and save the result
63-
return completeDeviceRegistration({ login, authTicket });
64+
return completeDeviceRegistration({ login, deviceName, authTicket });
6465
};

src/modules/crypto/keychainManager.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Database } from 'better-sqlite3';
22
import { Entry } from '@napi-rs/keyring';
33
import winston from 'winston';
4+
import os from 'os';
45
import crypto from 'crypto';
56
import { decrypt, getDerivateUsingParametersFromEncryptedData } from './decrypt';
67
import { encryptAES } from './encrypt';
@@ -85,7 +86,10 @@ const getSecretsWithoutDB = async (
8586
const localKey = generateLocalKey();
8687

8788
// Register the user's device
88-
const { deviceAccessKey, deviceSecretKey, serverKey } = await registerDevice({ login });
89+
const { deviceAccessKey, deviceSecretKey, serverKey } = await registerDevice({
90+
login,
91+
deviceName: `${os.hostname()} - ${os.platform()}-${os.arch()}`,
92+
});
8993

9094
// Get the authentication type (mainly to identify if the user is with OTP2)
9195
const { type } = await get2FAStatusUnauthenticated({ login });

0 commit comments

Comments
 (0)