Skip to content

Commit

Permalink
Add command to get secret from vault
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikescops committed Oct 16, 2023
1 parent 491d94e commit 9aa1f04
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/command-handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './inject';
export * from './logout';
export * from './passwords';
export * from './read';
export * from './secrets';
export * from './secureNotes';
export * from './sync';
export * from './teamDevices';
Expand Down
72 changes: 72 additions & 0 deletions src/command-handlers/secrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Database from 'better-sqlite3';
import winston from 'winston';
import { BackupEditTransaction, Secrets, SecretTransactionContent, VaultSecret } from '../types';
import { decryptTransactions } from '../modules/crypto';
import { askSecretChoice, filterMatches } from '../utils';
import { connectAndPrepare } from '../modules/database';

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

interface GetSecret {
filters: string[] | null;
secrets: Secrets;
output: 'text' | 'json';
db: Database.Database;
}

export const getSecret = async (params: GetSecret): Promise<void> => {
const { secrets, filters, db, output } = params;

winston.debug(`Retrieving: ${filters && filters.length > 0 ? filters.join(' ') : ''}`);
const transactions = db
.prepare(`SELECT * FROM transactions WHERE login = ? AND type = 'SECRET' AND action = 'BACKUP_EDIT'`)
.bind(secrets.login)
.all() as BackupEditTransaction[];

const secretDecrypted = await decryptTransactions<SecretTransactionContent>(transactions, secrets);

// transform entries [{_attributes: {key: xx}, _cdata: ww}] into an easier-to-use object
const beautifiedNotes = secretDecrypted?.map(
(item) =>
Object.fromEntries(
item.root.KWSecret.KWDataItem.map((entry) => [
entry._attributes.key[0].toLowerCase() + entry._attributes.key.slice(1), // lowercase the first letter: OtpSecret => otpSecret
entry._cdata,
])
) as unknown as VaultSecret
);

let matchedSecrets = filterMatches<VaultSecret>(beautifiedNotes, filters, ['title']);

switch (output) {
case 'json':
console.log(JSON.stringify(matchedSecrets));
break;
case 'text': {
let selectedSecret: VaultSecret | null = null;

if (!matchedSecrets || matchedSecrets.length === 0) {
throw new Error('No secret found');
} else if (matchedSecrets.length === 1) {
selectedSecret = matchedSecrets[0];
} else {
matchedSecrets = matchedSecrets?.sort();
selectedSecret = await askSecretChoice({ matchedSecrets, hasFilters: Boolean(filters?.length) });
}

console.log(selectedSecret.content);
break;
}
default:
throw new Error('Unable to recognize the output mode.');
}
};
18 changes: 18 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
runInject,
runExec,
runBackup,
runSecret,
} from '../command-handlers';

export const rootCommands = (params: { program: Command }) => {
Expand Down Expand Up @@ -86,6 +87,23 @@ export const rootCommands = (params: { program: Command }) => {
)
.action(runSecureNote);

program
.command('secret')
.description('Retrieve a secret from the local vault and open it')
.addOption(
new Option(
'-o, --output <type>',
'How to print the secrets. The JSON option outputs all the matching secrets'
)
.choices(['text', 'json'])
.default('text')
)
.argument(
'[filters...]',
'Filter secrets based on any parameter using <param>=<value>; if <param> is not specified in the filter, will default to title only'
)
.action(runSecret);

accountsCommands({ program });

devicesCommands({ program });
Expand Down
48 changes: 47 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ export interface BackupRemoveTransaction {
action: 'BACKUP_REMOVE';
}

export type TransactionContent = AuthentifiantTransactionContent | SecureNoteTransactionContent;
export type TransactionContent =
| AuthentifiantTransactionContent
| SecureNoteTransactionContent
| SecretTransactionContent;

export interface AuthentifiantTransactionContent {
root: {
Expand Down Expand Up @@ -134,6 +137,19 @@ export interface SecureNoteTransactionContent {
};
}

export interface SecretTransactionContent {
root: {
KWSecret: {
KWDataItem: {
_attributes: {
key: string;
};
_cdata?: string;
}[];
};
};
}

export interface VaultCredential {
title?: string;
email?: string;
Expand Down Expand Up @@ -210,6 +226,36 @@ export class PrintableVaultNote {
}
}

export interface VaultSecret {
anonId: string;
category?: string;
content: string;
creationDate?: string;
creationDateTime?: string;
id: string;
lastBackupTime: string;
secured: string; // either true or false
spaceId?: string;
title: string;
updateDate?: string;
localeFormat: string; // either UNIVERSAL or a country code
type: string;
sharedObject?: string;
userModificationDatetime?: string;
}

export class PrintableVaultSecret {
vaultSecret: VaultSecret;

constructor(vaultSecret: VaultSecret) {
this.vaultSecret = vaultSecret;
}

toString(): string {
return this.vaultSecret.title.trim();
}
}

export interface VaultSecrets {
credentials: VaultCredential[];
notes: VaultNote[];
Expand Down
29 changes: 28 additions & 1 deletion src/utils/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import inquirer from 'inquirer';
import inquirerSearchList from 'inquirer-search-list';
import { removeUnderscoresAndCapitalize } from './strings';
import { getDeviceCredentials } from './deviceCredentials';
import { PrintableVaultCredential, PrintableVaultNote, VaultCredential, VaultNote } from '../types';
import {
PrintableVaultCredential,
PrintableVaultNote,
PrintableVaultSecret,
VaultCredential,
VaultNote,
VaultSecret,
} from '../types';
import { GetAuthenticationMethodsForDeviceResult } from '../endpoints/getAuthenticationMethodsForDevice';
import PromptConstructor = inquirer.prompts.PromptConstructor;

Expand Down Expand Up @@ -124,6 +131,26 @@ export const askSecureNoteChoice = async (params: { matchedNotes: VaultNote[]; h
return response.printableNote.vaultNote;
};

export const askSecretChoice = async (params: { matchedSecrets: VaultSecret[]; hasFilters: boolean }) => {
const message = params.hasFilters
? 'There are multiple results for your query, pick one:'
: 'What secret would you like to get?';

const response = await prompt<{ printableSecret: PrintableVaultSecret }>([
{
type: 'search-list',
name: 'printableSecret',
message,
choices: params.matchedSecrets.map((item) => {
const printableItem = new PrintableVaultSecret(item);
return { name: printableItem.toString(), value: printableItem };
}),
},
]);

return response.printableSecret.vaultSecret;
};

export const askOtp = async () => {
const response = await prompt<{ otp: string }>([
{
Expand Down

0 comments on commit 9aa1f04

Please sign in to comment.