Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions documentation/pages/personal/vault.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ dcli secret [filters]
dcli secret title=api_keys -o json
```

## Get a personal ID

The `id` command retrieves structured personal identity items stored in your vault:
passports, ID cards, identity records, driver's licenses, social security IDs, and tax
numbers (fiscal statements). Use filters the same way as the other commands. By default
it matches on the item type and name fields; you can also select a different output among
`text, json`. The JSON option outputs all matching items with every field they contain.

```sh copy
dcli id [filters]
```

```sh copy filename="Return every personal ID item as JSON"
dcli id -o json
```

```sh copy filename="Return only passports"
dcli id type=passport
```

```sh copy filename="Return the tax number for someone named John"
dcli id type=fiscalstatement firstName=john
```

## Options

By default an automatic synchronization is performed once per hour.
Expand Down
1 change: 1 addition & 0 deletions src/command-handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './lock.js';
export * from './logout.js';
export * from './teamMcp.js';
export * from './passwords.js';
export * from './personalInfo.js';
export * from './read.js';
export * from './publicAPI.js';
export * from './secrets.js';
Expand Down
90 changes: 90 additions & 0 deletions src/command-handlers/personalInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Database from 'better-sqlite3';
import { BackupEditTransaction, LocalConfiguration, PersonalInfoTransactionContent, VaultId } from '../types.js';
import { decryptTransactions } from '../modules/crypto/index.js';
import { filterMatches } from '../utils/index.js';
import { connectAndPrepare } from '../modules/database/index.js';
import { logger } from '../logger.js';

/** Vault transaction types that hold structured personal identity information. */
export const PERSONAL_INFO_TYPES = [
'IDENTITY',
'PASSPORT',
'IDCARD',
'DRIVERLICENSE',
'SOCIALSECURITYID',
'FISCALSTATEMENT',
] as const;

export const runId = async (filters: string[] | null, options: { output: 'text' | 'json' }) => {
const { db, localConfiguration } = await connectAndPrepare({});
const ids = await getIds({ filters, localConfiguration, db });
db.close();

switch (options.output) {
case 'json':
logger.content(JSON.stringify(ids));
break;
case 'text': {
if (ids.length === 0) {
throw new Error('No personal ID item found');
}
const blocks = ids.map((id) => {
const lines = Object.entries(id)
.filter(([, value]) => typeof value === 'string' && value.length > 0)
.map(([key, value]) => ` ${key}: ${value}`);
return `[${id.type}]\n${lines.join('\n')}`;
});
logger.content(blocks.join('\n\n'));
break;
}
default:
throw new Error('Unable to recognize the output mode.');
}
};

interface GetIds {
filters: string[] | null;
localConfiguration: LocalConfiguration;
db: Database.Database;
}

export const getIds = async (params: GetIds): Promise<VaultId[]> => {
const { localConfiguration, filters, db } = params;

logger.debug(`Retrieving personal ID items: ${filters && filters.length > 0 ? filters.join(' ') : ''}`);

const placeholders = PERSONAL_INFO_TYPES.map(() => '?').join(', ');
const transactions = db
.prepare(`SELECT * FROM transactions WHERE login = ? AND type IN (${placeholders}) AND action = 'BACKUP_EDIT'`)
.bind(localConfiguration.login, ...PERSONAL_INFO_TYPES)
.all() as BackupEditTransaction[];

const decrypted = await decryptTransactions<PersonalInfoTransactionContent>(transactions, localConfiguration);

// Each item's content is a single root element (KWPassport, KWIDCard, KWIdentity, ...) whose
// KWDataItem entries are the fields. This is type-agnostic: any field the item carries is exposed.
const beautifiedIds = decrypted.map((item, index) => {
const kwType = Object.keys(item.root)[0];
const dataItems = item.root[kwType]?.KWDataItem ?? [];
const fields = Object.fromEntries(
dataItems
.filter((entry) => entry._cdata !== undefined)
.map((entry) => [
entry._attributes.key[0].toLowerCase() + entry._attributes.key.slice(1), // FirstName => firstName
entry._cdata as string,
])
) as Record<string, string>;

// Transaction type (PASSPORT, IDCARD, ...) is the primary, reliable classifier and filter key.
return { ...fields, kwType, type: transactions[index].type } as VaultId;
});

return filterMatches<VaultId>(beautifiedIds, filters, [
'type',
'fullname',
'firstName',
'lastName',
'number',
'idNumber',
]);
};
17 changes: 17 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
runSync,
runPassword,
runSecureNote,
runId,
runLock,
runLogout,
runRead,
Expand Down Expand Up @@ -101,6 +102,22 @@ export const rootCommands = (params: { program: Command }) => {
)
.action(runSecret);

program
.command('id')
.description(
'Retrieve a personal ID item (passport, ID card, identity, driver license, tax number, ...) from the local vault'
)
.addOption(
new Option('-o, --output <type>', 'How to print the IDs. The JSON option outputs all the matching items')
.choices(['text', 'json'])
.default('text')
)
.argument(
'[filters...]',
'Filter items based on any parameter using <param>=<value>; if <param> is not specified in the filter, will default to type and name fields'
)
.action(runId);

accountsCommands({ program });

devicesCommands({ program });
Expand Down
33 changes: 32 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ export interface BackupRemoveTransaction {
export type TransactionContent =
| AuthentifiantTransactionContent
| SecureNoteTransactionContent
| SecretTransactionContent;
| SecretTransactionContent
| PersonalInfoTransactionContent;

export interface AuthentifiantTransactionContent {
root: {
Expand Down Expand Up @@ -162,6 +163,36 @@ export interface SecretTransactionContent {
};
}

/**
* Structured personal identity items (passport, ID card, identity, driver's license,
* social security id, fiscal statement). The root element name varies by type
* (KWPassport, KWIDCard, KWIdentity, ...), so it is keyed generically.
*/
export interface PersonalInfoTransactionContent {
root: Record<
string,
{
KWDataItem: {
_attributes: {
key: string;
};
_cdata?: string;
}[];
}
>;
}

/**
* A decrypted personal identity item. Fields vary by item type, so all decrypted
* fields are exposed dynamically; `type` is the transaction type (PASSPORT, IDCARD, ...)
* and `kwType` is the content root element name (KWPassport, KWIDCard, ...).
*/
export interface VaultId {
type: string;
kwType: string;
[field: string]: string;
}

export interface VaultCredential {
title?: string;
email?: string;
Expand Down