favacli 0.0.14 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import BaseCommand from '../../BaseCommand.mjs';
|
|
2
|
+
declare class ExportTextCommand extends BaseCommand {
|
|
3
|
+
static paths: string[][];
|
|
4
|
+
requireTwoFaLib: boolean;
|
|
5
|
+
pathOption: string | undefined;
|
|
6
|
+
passphraseSource: string | undefined;
|
|
7
|
+
static usage: import("clipanion").Usage;
|
|
8
|
+
exec(): Promise<{
|
|
9
|
+
success: boolean;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export default ExportTextCommand;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { Option } from 'clipanion';
|
|
3
|
+
import * as t from 'typanion';
|
|
4
|
+
import keytar from 'keytar';
|
|
5
|
+
import BaseCommand from '../../BaseCommand.mjs';
|
|
6
|
+
import { password, input } from '@inquirer/prompts';
|
|
7
|
+
class ExportTextCommand extends BaseCommand {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.requireTwoFaLib = true;
|
|
11
|
+
this.pathOption = Option.String('--path', {
|
|
12
|
+
description: 'File path for the export, if not set will output to stdout',
|
|
13
|
+
});
|
|
14
|
+
this.passphraseSource = Option.String('--passphrase-source', {
|
|
15
|
+
description: 'Source of passphrase, either "stdin" or "stored"',
|
|
16
|
+
validator: t.isEnum(['stdin', 'stored']),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
static { this.paths = [['export', 'text']]; }
|
|
20
|
+
static { this.usage = BaseCommand.Usage({
|
|
21
|
+
category: 'Export',
|
|
22
|
+
description: 'Export 2FA entries as plain text',
|
|
23
|
+
details: `
|
|
24
|
+
This command exports your 2FA entries as a plain text file.
|
|
25
|
+
|
|
26
|
+
You can specify a file path for the export using the --path flag.
|
|
27
|
+
|
|
28
|
+
You can specify the passphrase source with --passphrase-source flag:
|
|
29
|
+
- "stdin": You'll be prompted to enter a passphrase
|
|
30
|
+
- "stored": Will use the previously stored passphrase, if none is found, will be promted for a passphrase
|
|
31
|
+
|
|
32
|
+
If no passphrase source is specified, it will export without encryption (no passphrase).
|
|
33
|
+
|
|
34
|
+
WARNING: Exporting your 2FA secrets in plain text (without passphrase) is not secure.
|
|
35
|
+
`,
|
|
36
|
+
examples: [
|
|
37
|
+
['Export entries as unencrypted text (UNSECURE)', 'export text'],
|
|
38
|
+
['Export to specific file', 'export text --path=/path/to/export.txt'],
|
|
39
|
+
[
|
|
40
|
+
'Export using stored passphrase',
|
|
41
|
+
'export text --passphrase-source=stored',
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
'Export with manual passphrase entry',
|
|
45
|
+
'export text --passphrase-source=stdin',
|
|
46
|
+
],
|
|
47
|
+
],
|
|
48
|
+
}); }
|
|
49
|
+
async exec() {
|
|
50
|
+
const entriesCount = this.twoFaLib.vault.listEntries().length;
|
|
51
|
+
// Get passphrase based on source
|
|
52
|
+
let passphrase = undefined;
|
|
53
|
+
// If passphrase source is "stored", try to get from keytar without prompting
|
|
54
|
+
if (this.passphraseSource === 'stored') {
|
|
55
|
+
try {
|
|
56
|
+
passphrase = await keytar.getPassword('favacli', 'export-passphrase');
|
|
57
|
+
if (!passphrase) {
|
|
58
|
+
this.context.stderr.write('No stored passphrase found. Please enter a passphrase to store.\n');
|
|
59
|
+
passphrase = await password({
|
|
60
|
+
message: 'Enter passphrase to store and use for encryption:',
|
|
61
|
+
});
|
|
62
|
+
// Store the passphrase for future use
|
|
63
|
+
await keytar.setPassword('favacli', 'export-passphrase', passphrase);
|
|
64
|
+
this.context.stdout.write('Passphrase stored.\n');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof Error) {
|
|
69
|
+
this.context.stderr.write(`Failed to access stored passphrase: ${error.message}}\n`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.context.stderr.write(`Failed to access stored passphrase: Unknown error\n`);
|
|
73
|
+
}
|
|
74
|
+
return { success: false };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Default behavior or explicit "stdin"
|
|
78
|
+
else if (this.passphraseSource === 'stdin') {
|
|
79
|
+
passphrase = await password({
|
|
80
|
+
message: 'Enter passphrase to use for encryption:',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Add warning and confirmation if no passphrase is provided
|
|
84
|
+
else if (!this.passphraseSource) {
|
|
85
|
+
this.context.stderr.write('WARNING: You are about to export 2FA secrets in plain text without encryption.\n');
|
|
86
|
+
this.context.stderr.write('This is NOT SECURE and could expose your 2FA secrets if the file is accessed by others.\n');
|
|
87
|
+
const confirm = await input({
|
|
88
|
+
message: 'Type "unsecure" to acknowldge you understand the risks and to proceed with unencrypted export:',
|
|
89
|
+
});
|
|
90
|
+
if (confirm !== 'unsecure') {
|
|
91
|
+
this.context.stderr.write('Export cancelled.\n');
|
|
92
|
+
return { success: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const content = await this.twoFaLib.exportImport.exportEntries('text', passphrase, true);
|
|
96
|
+
if (this.pathOption) {
|
|
97
|
+
try {
|
|
98
|
+
await fs.writeFile(this.pathOption, content);
|
|
99
|
+
this.context.stdout.write(`Successfully exported ${entriesCount} entries to ${this.pathOption}\n`);
|
|
100
|
+
return { success: true };
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof Error) {
|
|
104
|
+
this.context.stderr.write(`Failed to export: ${error.message}\n`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.context.stderr.write('Failed to export: Unknown error\n');
|
|
108
|
+
}
|
|
109
|
+
return { success: false };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.context.stdout.write(content);
|
|
114
|
+
return { success: true };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export default ExportTextCommand;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import BaseCommand from '../../BaseCommand.mjs';
|
|
2
|
+
declare class ResilverCommand extends BaseCommand {
|
|
3
|
+
static paths: string[][];
|
|
4
|
+
requireTwoFaLib: boolean;
|
|
5
|
+
static usage: import("clipanion").Usage;
|
|
6
|
+
exec(): Promise<{
|
|
7
|
+
success: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export default ResilverCommand;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import BaseCommand from '../../BaseCommand.mjs';
|
|
2
|
+
class ResilverCommand extends BaseCommand {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.requireTwoFaLib = true;
|
|
6
|
+
}
|
|
7
|
+
static { this.paths = [['sync', 'resilver']]; }
|
|
8
|
+
static { this.usage = BaseCommand.Usage({
|
|
9
|
+
category: 'Sync',
|
|
10
|
+
description: 'Resilver a vault',
|
|
11
|
+
details: `This command allows you to resync a vault to the server. This is useful if some devices have become desynced.`,
|
|
12
|
+
examples: [['Resilver a vault', 'sync resilver']],
|
|
13
|
+
}); }
|
|
14
|
+
async exec() {
|
|
15
|
+
if (!this.twoFaLib.sync) {
|
|
16
|
+
throw new Error('No server url set');
|
|
17
|
+
}
|
|
18
|
+
this.twoFaLib.sync.requestResilver();
|
|
19
|
+
// TODO: do this based on events
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
21
|
+
return { success: true };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export default ResilverCommand;
|
package/build/main.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import EntriesListCommand from './commands/entries/list.mjs';
|
|
|
7
7
|
import EntriesSearchCommand from './commands/entries/search.mjs';
|
|
8
8
|
import SyncSetServerUrlCommand from './commands/sync/setServerUrl.mjs';
|
|
9
9
|
import SyncConnect from './commands/sync/connect.mjs';
|
|
10
|
+
import SyncResilver from './commands/sync/resilver.mjs';
|
|
11
|
+
import ExportTextCommand from './commands/export/text.mjs';
|
|
10
12
|
// check node version
|
|
11
13
|
const nodeRuntimeMajorVersion = parseInt(process.version.split('.')[0]);
|
|
12
14
|
if (nodeRuntimeMajorVersion < 20) {
|
|
@@ -16,7 +18,7 @@ const [, , ...args] = process.argv;
|
|
|
16
18
|
const cli = new Cli({
|
|
17
19
|
binaryLabel: 'FavaCli',
|
|
18
20
|
binaryName: `favacli`,
|
|
19
|
-
binaryVersion: '0.0.
|
|
21
|
+
binaryVersion: '0.0.15',
|
|
20
22
|
});
|
|
21
23
|
cli.register(VaultCreateCommand);
|
|
22
24
|
cli.register(VaultDeleteCommand);
|
|
@@ -25,5 +27,7 @@ cli.register(EntriesListCommand);
|
|
|
25
27
|
cli.register(EntriesSearchCommand);
|
|
26
28
|
cli.register(SyncSetServerUrlCommand);
|
|
27
29
|
cli.register(SyncConnect);
|
|
30
|
+
cli.register(SyncResilver);
|
|
31
|
+
cli.register(ExportTextCommand);
|
|
28
32
|
cli.register(Builtins.HelpCommand);
|
|
29
33
|
void cli.runExit(args);
|
|
@@ -9,16 +9,26 @@ const loadVault = async (vaultData, settings, verbose = false) => {
|
|
|
9
9
|
const twoFaLib = await twoFaLibVaultCreationUtils.loadTwoFaLibFromLockedRepesentation(vaultData, passphrase);
|
|
10
10
|
twoFaLib.addEventListener(TwoFaLibEvent.Changed, async (ev) => {
|
|
11
11
|
const tempFile = `${settings.vaultLocation}.tmp`;
|
|
12
|
+
const backupFile = `${settings.vaultLocation}.backup`;
|
|
12
13
|
try {
|
|
13
14
|
// Write to temporary file first, so we don't have to worry about partial writes
|
|
14
15
|
await fs.writeFile(tempFile, ev.detail.newLockedRepresentationString);
|
|
16
|
+
// Create backup of existing vault if it exists
|
|
17
|
+
try {
|
|
18
|
+
await fs.copyFile(settings.vaultLocation, backupFile);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
// If the error is not because the original file doesn't exist yet, throw it
|
|
22
|
+
if (err instanceof Error && 'code' in err && err.code !== 'ENOENT')
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
15
25
|
// Atomically rename temp file to target file
|
|
16
26
|
await fs.rename(tempFile, settings.vaultLocation);
|
|
17
27
|
}
|
|
18
28
|
catch (error) {
|
|
19
29
|
// Clean up temp file if something went wrong
|
|
20
30
|
await fs.unlink(tempFile).catch((err) => {
|
|
21
|
-
console.error(err);
|
|
31
|
+
console.error('Failed to clean up temp file:', err);
|
|
22
32
|
});
|
|
23
33
|
throw error;
|
|
24
34
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "favacli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"bufferutil": "^4.0.8",
|
|
11
11
|
"clipanion": "^4.0.0-rc.4",
|
|
12
12
|
"env-paths": "^3.0.0",
|
|
13
|
-
"favalib": "^0.0.
|
|
13
|
+
"favalib": "^0.0.5",
|
|
14
14
|
"keytar": "^7.9.0",
|
|
15
15
|
"node-loader": "^2.0.0",
|
|
16
16
|
"ts-loader": "^9.5.1",
|