autho 3.0.0 → 3.0.1
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.
- package/README.md +3 -1
- package/dist/autho.js +987 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,9 @@ autho secrets get --password "..." --ref github --json
|
|
|
28
28
|
autho otp code --password "..." --ref my-totp --json
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Run `autho init` to save your master password to the native OS secret store (macOS Keychain, Linux Secret Service, Windows Credential Manager). After that, all commands unlock silently without prompting.
|
|
32
|
+
|
|
33
|
+
You can also set `AUTHO_MASTER_PASSWORD` to avoid passing `--password` on every call, or set `AUTHO_DISABLE_OS_SECRETS=1` to opt out of OS secret storage.
|
|
32
34
|
|
|
33
35
|
## Features
|
|
34
36
|
|
package/dist/autho.js
CHANGED
|
@@ -15,6 +15,7 @@ var __export = (target, all) => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = import.meta.require;
|
|
18
19
|
|
|
19
20
|
// packages/storage/src/index.ts
|
|
20
21
|
import { Database } from "bun:sqlite";
|
|
@@ -105,6 +106,13 @@ class AuthoDatabase {
|
|
|
105
106
|
setVaultConfig(config) {
|
|
106
107
|
this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)").run("vault.config", JSON.stringify(config));
|
|
107
108
|
}
|
|
109
|
+
getVaultAuth() {
|
|
110
|
+
const row = this.db.query("SELECT value FROM meta WHERE key = ?1").get("vault.auth");
|
|
111
|
+
return row ? parseJson(row.value) : null;
|
|
112
|
+
}
|
|
113
|
+
setVaultAuth(auth) {
|
|
114
|
+
this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)").run("vault.auth", JSON.stringify(auth));
|
|
115
|
+
}
|
|
108
116
|
countSecrets() {
|
|
109
117
|
const row = this.db.query("SELECT COUNT(*) AS count FROM secrets").get();
|
|
110
118
|
return row.count;
|
|
@@ -221,6 +229,7 @@ var init_src = () => {};
|
|
|
221
229
|
import {
|
|
222
230
|
createCipheriv,
|
|
223
231
|
createDecipheriv,
|
|
232
|
+
createHmac,
|
|
224
233
|
randomBytes,
|
|
225
234
|
scryptSync
|
|
226
235
|
} from "crypto";
|
|
@@ -280,6 +289,78 @@ function unlockRootKey(password, config) {
|
|
|
280
289
|
const key = deriveKeyFromPassword(password, config.kdf);
|
|
281
290
|
return decryptWithKey(config.wrappedRootKey, key, "autho:vault-root");
|
|
282
291
|
}
|
|
292
|
+
function decodeBase32(input) {
|
|
293
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
294
|
+
const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
|
|
295
|
+
let bits = 0;
|
|
296
|
+
let value = 0;
|
|
297
|
+
const output = [];
|
|
298
|
+
for (const char of normalized) {
|
|
299
|
+
const index = alphabet.indexOf(char);
|
|
300
|
+
if (index === -1) {
|
|
301
|
+
throw new Error("OTP secret must be valid base32");
|
|
302
|
+
}
|
|
303
|
+
value = value << 5 | index;
|
|
304
|
+
bits += 5;
|
|
305
|
+
if (bits >= 8) {
|
|
306
|
+
output.push(value >>> bits - 8 & 255);
|
|
307
|
+
bits -= 8;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return Uint8Array.from(output);
|
|
311
|
+
}
|
|
312
|
+
function generateTotpCode(secret, options, now) {
|
|
313
|
+
const algorithm = (options?.algorithm ?? "sha1").toLowerCase();
|
|
314
|
+
const digits = options?.digits ?? 6;
|
|
315
|
+
const key = decodeBase32(secret);
|
|
316
|
+
const counter = Math.floor(now / 30000);
|
|
317
|
+
const message = Buffer.alloc(8);
|
|
318
|
+
let cursor = counter;
|
|
319
|
+
for (let index = 7;index >= 0; index -= 1) {
|
|
320
|
+
message[index] = cursor & 255;
|
|
321
|
+
cursor >>= 8;
|
|
322
|
+
}
|
|
323
|
+
const hash = createHmac(algorithm, Buffer.from(key)).update(message).digest();
|
|
324
|
+
const offset = hash[hash.length - 1] & 15;
|
|
325
|
+
const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
|
|
326
|
+
const mod = 10 ** digits;
|
|
327
|
+
return String(binary % mod).padStart(digits, "0");
|
|
328
|
+
}
|
|
329
|
+
function generateTotpSecret() {
|
|
330
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
331
|
+
const bytes = randomBytes(20);
|
|
332
|
+
let bits = 0;
|
|
333
|
+
let value = 0;
|
|
334
|
+
let result = "";
|
|
335
|
+
for (const byte of bytes) {
|
|
336
|
+
value = value << 8 | byte;
|
|
337
|
+
bits += 8;
|
|
338
|
+
while (bits >= 5) {
|
|
339
|
+
result += alphabet[value >>> bits - 5 & 31];
|
|
340
|
+
bits -= 5;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (bits > 0) {
|
|
344
|
+
result += alphabet[value << 5 - bits & 31];
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
function totpUri(secret, issuer, account) {
|
|
349
|
+
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(account)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
|
|
350
|
+
}
|
|
351
|
+
function verifyTotpCode(secret, code, opts) {
|
|
352
|
+
const digits = opts?.digits ?? 6;
|
|
353
|
+
if (code.length !== digits) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
const now = Date.now();
|
|
357
|
+
for (const offset of [-30000, 0, 30000]) {
|
|
358
|
+
if (generateTotpCode(secret, opts, now + offset) === code) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
283
364
|
var DEFAULT_KDF;
|
|
284
365
|
var init_src2 = __esm(() => {
|
|
285
366
|
DEFAULT_KDF = {
|
|
@@ -465,7 +546,7 @@ var init_artifacts = __esm(() => {
|
|
|
465
546
|
|
|
466
547
|
// packages/core/src/index.ts
|
|
467
548
|
import { spawnSync } from "child_process";
|
|
468
|
-
import { createHmac, randomBytes as randomBytes3 } from "crypto";
|
|
549
|
+
import { createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
|
|
469
550
|
import {
|
|
470
551
|
existsSync as existsSync2,
|
|
471
552
|
readFileSync as readFileSync2
|
|
@@ -477,7 +558,7 @@ function requireValue(value, label) {
|
|
|
477
558
|
}
|
|
478
559
|
return value;
|
|
479
560
|
}
|
|
480
|
-
function
|
|
561
|
+
function decodeBase322(input) {
|
|
481
562
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
482
563
|
const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
|
|
483
564
|
let bits = 0;
|
|
@@ -500,7 +581,7 @@ function decodeBase32(input) {
|
|
|
500
581
|
function generateTotp(secret, options, now = Date.now()) {
|
|
501
582
|
const algorithm = (options?.algorithm ?? "sha1").toLowerCase();
|
|
502
583
|
const digits = options?.digits ?? 6;
|
|
503
|
-
const key =
|
|
584
|
+
const key = decodeBase322(secret);
|
|
504
585
|
const counter = Math.floor(now / 30000);
|
|
505
586
|
const message = Buffer.alloc(8);
|
|
506
587
|
let cursor = counter;
|
|
@@ -508,7 +589,7 @@ function generateTotp(secret, options, now = Date.now()) {
|
|
|
508
589
|
message[index] = cursor & 255;
|
|
509
590
|
cursor >>= 8;
|
|
510
591
|
}
|
|
511
|
-
const hash =
|
|
592
|
+
const hash = createHmac2(algorithm, Buffer.from(key)).update(message).digest();
|
|
512
593
|
const offset = hash[hash.length - 1] & 15;
|
|
513
594
|
const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
|
|
514
595
|
const mod = 10 ** digits;
|
|
@@ -710,21 +791,212 @@ class VaultService {
|
|
|
710
791
|
db.close();
|
|
711
792
|
}
|
|
712
793
|
}
|
|
713
|
-
static unlock(vaultPath,
|
|
794
|
+
static unlock(vaultPath, credentials) {
|
|
795
|
+
const creds = typeof credentials === "string" ? { password: credentials } : credentials;
|
|
714
796
|
const db = new AuthoDatabase(vaultPath);
|
|
715
797
|
const config = db.getVaultConfig();
|
|
716
798
|
if (!config) {
|
|
717
799
|
db.close();
|
|
718
800
|
throw new Error(`Vault is not initialized at ${vaultPath}`);
|
|
719
801
|
}
|
|
802
|
+
const vaultAuth = db.getVaultAuth();
|
|
803
|
+
if (creds.recovery) {
|
|
804
|
+
if (!vaultAuth?.recovery) {
|
|
805
|
+
db.close();
|
|
806
|
+
throw new Error("No recovery token configured for this vault");
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
const recoveryKEK = deriveKeyFromPassword(creds.recovery, vaultAuth.recovery.kdf);
|
|
810
|
+
const rootKey = decryptWithKey(vaultAuth.recovery.wrappedRootKey, recoveryKEK, "autho:vault-recovery");
|
|
811
|
+
db.insertAudit({
|
|
812
|
+
createdAt: new Date().toISOString(),
|
|
813
|
+
eventType: "auth.unlock.recovery",
|
|
814
|
+
id: randomId(),
|
|
815
|
+
message: "Vault unlocked via recovery file",
|
|
816
|
+
metadata: JSON.stringify({}),
|
|
817
|
+
subjectRef: null,
|
|
818
|
+
subjectType: "vault"
|
|
819
|
+
});
|
|
820
|
+
return new VaultSession(db, rootKey);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
db.close();
|
|
823
|
+
throw new Error("Invalid recovery token", { cause: error });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
720
826
|
try {
|
|
721
|
-
const
|
|
827
|
+
const passwordKEK = deriveKeyFromPassword(creds.password, config.kdf);
|
|
828
|
+
if (vaultAuth?.totp) {
|
|
829
|
+
const totpSecret = decryptWithKey(vaultAuth.totp.encryptedSecret, passwordKEK, "autho:vault-totp").toString("utf8");
|
|
830
|
+
if (!creds.totp || !verifyTotpCode(totpSecret, creds.totp)) {
|
|
831
|
+
throw new Error("Invalid or missing TOTP code");
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const rootKey = decryptWithKey(config.wrappedRootKey, passwordKEK, "autho:vault-root");
|
|
722
835
|
return new VaultSession(db, rootKey);
|
|
723
836
|
} catch (error) {
|
|
724
837
|
db.close();
|
|
838
|
+
if (error instanceof Error && error.message === "Invalid or missing TOTP code") {
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
725
841
|
throw new Error("Invalid vault password", { cause: error });
|
|
726
842
|
}
|
|
727
843
|
}
|
|
844
|
+
static getAuthConfig(vaultPath) {
|
|
845
|
+
const db = new AuthoDatabase(vaultPath);
|
|
846
|
+
try {
|
|
847
|
+
return db.getVaultAuth();
|
|
848
|
+
} finally {
|
|
849
|
+
db.close();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
static setupTotp(vaultPath) {
|
|
853
|
+
const secret = generateTotpSecret();
|
|
854
|
+
const uri = totpUri(secret, "autho", vaultPath);
|
|
855
|
+
return { secret, uri };
|
|
856
|
+
}
|
|
857
|
+
static enableTotp(vaultPath, credentials, secret, code) {
|
|
858
|
+
if (!verifyTotpCode(secret, code)) {
|
|
859
|
+
throw new Error("Invalid TOTP code \u2014 make sure your authenticator app is showing the right code");
|
|
860
|
+
}
|
|
861
|
+
const session = VaultService.unlock(vaultPath, credentials);
|
|
862
|
+
session.close();
|
|
863
|
+
const db = new AuthoDatabase(vaultPath);
|
|
864
|
+
try {
|
|
865
|
+
const config = db.getVaultConfig();
|
|
866
|
+
const passwordKEK = deriveKeyFromPassword(credentials.password, config.kdf);
|
|
867
|
+
const encryptedSecret = encryptWithKey(Buffer.from(secret, "utf8"), passwordKEK, "autho:vault-totp");
|
|
868
|
+
const existing = db.getVaultAuth();
|
|
869
|
+
db.setVaultAuth({
|
|
870
|
+
version: 1,
|
|
871
|
+
...existing,
|
|
872
|
+
totp: {
|
|
873
|
+
algorithm: "SHA1",
|
|
874
|
+
digits: 6,
|
|
875
|
+
encryptedSecret,
|
|
876
|
+
period: 30
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
db.insertAudit({
|
|
880
|
+
createdAt: new Date().toISOString(),
|
|
881
|
+
eventType: "auth.totp.enabled",
|
|
882
|
+
id: randomId(),
|
|
883
|
+
message: "TOTP vault unlock enabled",
|
|
884
|
+
metadata: JSON.stringify({}),
|
|
885
|
+
subjectRef: null,
|
|
886
|
+
subjectType: "vault"
|
|
887
|
+
});
|
|
888
|
+
} finally {
|
|
889
|
+
db.close();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
static removeTotp(vaultPath, credentials) {
|
|
893
|
+
const session = VaultService.unlock(vaultPath, credentials);
|
|
894
|
+
session.close();
|
|
895
|
+
const db = new AuthoDatabase(vaultPath);
|
|
896
|
+
try {
|
|
897
|
+
const existing = db.getVaultAuth();
|
|
898
|
+
if (existing) {
|
|
899
|
+
const { totp: _removed, ...rest } = existing;
|
|
900
|
+
db.setVaultAuth({ ...rest, version: 1 });
|
|
901
|
+
}
|
|
902
|
+
db.insertAudit({
|
|
903
|
+
createdAt: new Date().toISOString(),
|
|
904
|
+
eventType: "auth.totp.removed",
|
|
905
|
+
id: randomId(),
|
|
906
|
+
message: "TOTP vault unlock removed",
|
|
907
|
+
metadata: JSON.stringify({}),
|
|
908
|
+
subjectRef: null,
|
|
909
|
+
subjectType: "vault"
|
|
910
|
+
});
|
|
911
|
+
} finally {
|
|
912
|
+
db.close();
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
static generateRecovery(vaultPath, credentials) {
|
|
916
|
+
const session = VaultService.unlock(vaultPath, credentials);
|
|
917
|
+
const rootKey = session.getRootKey();
|
|
918
|
+
session.close();
|
|
919
|
+
const db = new AuthoDatabase(vaultPath);
|
|
920
|
+
try {
|
|
921
|
+
const tokenBytes = randomBytes3(32);
|
|
922
|
+
const token = tokenBytes.toString("hex");
|
|
923
|
+
const recoverySalt = randomBytes3(16).toString("base64");
|
|
924
|
+
const recoveryKdf = {
|
|
925
|
+
keyLength: 32,
|
|
926
|
+
name: "scrypt",
|
|
927
|
+
salt: recoverySalt,
|
|
928
|
+
N: 1 << 17,
|
|
929
|
+
p: 1,
|
|
930
|
+
r: 8
|
|
931
|
+
};
|
|
932
|
+
const recoveryKEK = deriveKeyFromPassword(token, recoveryKdf);
|
|
933
|
+
const wrappedRootKey = encryptWithKey(rootKey, recoveryKEK, "autho:vault-recovery");
|
|
934
|
+
const existing = db.getVaultAuth();
|
|
935
|
+
db.setVaultAuth({
|
|
936
|
+
version: 1,
|
|
937
|
+
...existing,
|
|
938
|
+
recovery: {
|
|
939
|
+
createdAt: new Date().toISOString(),
|
|
940
|
+
kdf: recoveryKdf,
|
|
941
|
+
wrappedRootKey
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
const formattedToken = token.toUpperCase().match(/.{1,8}/g).join("-");
|
|
945
|
+
const fileContent = [
|
|
946
|
+
"================================================================================",
|
|
947
|
+
"AUTHO VAULT RECOVERY FILE",
|
|
948
|
+
"================================================================================",
|
|
949
|
+
`Generated : ${new Date().toISOString()}`,
|
|
950
|
+
`Vault : ${vaultPath}`,
|
|
951
|
+
"",
|
|
952
|
+
"WARNING: Anyone with this file can open your vault regardless of password,",
|
|
953
|
+
"PIN, or authenticator app. Store it offline (printed paper, encrypted USB,",
|
|
954
|
+
"safety deposit box). Revoke with: autho recovery revoke",
|
|
955
|
+
"",
|
|
956
|
+
"RECOVERY TOKEN:",
|
|
957
|
+
formattedToken,
|
|
958
|
+
"",
|
|
959
|
+
"To use: autho unlock --recovery-file <path-to-this-file>",
|
|
960
|
+
"================================================================================"
|
|
961
|
+
].join(`
|
|
962
|
+
`);
|
|
963
|
+
db.insertAudit({
|
|
964
|
+
createdAt: new Date().toISOString(),
|
|
965
|
+
eventType: "auth.recovery.generated",
|
|
966
|
+
id: randomId(),
|
|
967
|
+
message: "Recovery file generated",
|
|
968
|
+
metadata: JSON.stringify({}),
|
|
969
|
+
subjectRef: null,
|
|
970
|
+
subjectType: "vault"
|
|
971
|
+
});
|
|
972
|
+
return { fileContent };
|
|
973
|
+
} finally {
|
|
974
|
+
db.close();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
static revokeRecovery(vaultPath, credentials) {
|
|
978
|
+
const session = VaultService.unlock(vaultPath, credentials);
|
|
979
|
+
session.close();
|
|
980
|
+
const db = new AuthoDatabase(vaultPath);
|
|
981
|
+
try {
|
|
982
|
+
const existing = db.getVaultAuth();
|
|
983
|
+
if (existing) {
|
|
984
|
+
const { recovery: _removed, ...rest } = existing;
|
|
985
|
+
db.setVaultAuth({ ...rest, version: 1 });
|
|
986
|
+
}
|
|
987
|
+
db.insertAudit({
|
|
988
|
+
createdAt: new Date().toISOString(),
|
|
989
|
+
eventType: "auth.recovery.revoked",
|
|
990
|
+
id: randomId(),
|
|
991
|
+
message: "Recovery file revoked",
|
|
992
|
+
metadata: JSON.stringify({}),
|
|
993
|
+
subjectRef: null,
|
|
994
|
+
subjectType: "vault"
|
|
995
|
+
});
|
|
996
|
+
} finally {
|
|
997
|
+
db.close();
|
|
998
|
+
}
|
|
999
|
+
}
|
|
728
1000
|
}
|
|
729
1001
|
|
|
730
1002
|
class VaultSession {
|
|
@@ -734,6 +1006,9 @@ class VaultSession {
|
|
|
734
1006
|
this.db = db;
|
|
735
1007
|
this.rootKey = rootKey;
|
|
736
1008
|
}
|
|
1009
|
+
getRootKey() {
|
|
1010
|
+
return Buffer.from(this.rootKey);
|
|
1011
|
+
}
|
|
737
1012
|
close() {
|
|
738
1013
|
this.db.close();
|
|
739
1014
|
}
|
|
@@ -1126,6 +1401,146 @@ var init_src3 = __esm(() => {
|
|
|
1126
1401
|
init_paths();
|
|
1127
1402
|
});
|
|
1128
1403
|
|
|
1404
|
+
// packages/core/src/os-secrets.ts
|
|
1405
|
+
import { createHash as createHash2, randomBytes as randomBytes4, scryptSync as scryptSync2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1406
|
+
import { resolve as resolve3 } from "path";
|
|
1407
|
+
function vaultPasswordName(vaultPath) {
|
|
1408
|
+
return createHash2("sha256").update(resolve3(vaultPath)).digest("hex");
|
|
1409
|
+
}
|
|
1410
|
+
function osSecretsDisabled() {
|
|
1411
|
+
return process.env.AUTHO_DISABLE_OS_SECRETS === "1";
|
|
1412
|
+
}
|
|
1413
|
+
async function storeVaultPassword(vaultPath, password) {
|
|
1414
|
+
if (osSecretsDisabled()) {
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
await Bun.secrets.set({
|
|
1419
|
+
name: vaultPasswordName(vaultPath),
|
|
1420
|
+
service: VAULT_PASSWORD_SERVICE,
|
|
1421
|
+
value: password
|
|
1422
|
+
});
|
|
1423
|
+
return true;
|
|
1424
|
+
} catch {
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function loadVaultPassword(vaultPath) {
|
|
1429
|
+
if (osSecretsDisabled()) {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
try {
|
|
1433
|
+
const password = await Bun.secrets.get({
|
|
1434
|
+
name: vaultPasswordName(vaultPath),
|
|
1435
|
+
service: VAULT_PASSWORD_SERVICE
|
|
1436
|
+
});
|
|
1437
|
+
return password ?? null;
|
|
1438
|
+
} catch {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async function deleteVaultPassword(vaultPath) {
|
|
1443
|
+
if (osSecretsDisabled()) {
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
return await Bun.secrets.delete({
|
|
1448
|
+
name: vaultPasswordName(vaultPath),
|
|
1449
|
+
service: VAULT_PASSWORD_SERVICE
|
|
1450
|
+
});
|
|
1451
|
+
} catch {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function setOsSecret(name, value) {
|
|
1456
|
+
if (osSecretsDisabled()) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
try {
|
|
1460
|
+
await Bun.secrets.set({ name, service: USER_SECRETS_SERVICE, value });
|
|
1461
|
+
return true;
|
|
1462
|
+
} catch {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async function getOsSecret(name) {
|
|
1467
|
+
if (osSecretsDisabled()) {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
const value = await Bun.secrets.get({ name, service: USER_SECRETS_SERVICE });
|
|
1472
|
+
return value ?? null;
|
|
1473
|
+
} catch {
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
async function deleteOsSecret(name) {
|
|
1478
|
+
if (osSecretsDisabled()) {
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
try {
|
|
1482
|
+
return await Bun.secrets.delete({ name, service: USER_SECRETS_SERVICE });
|
|
1483
|
+
} catch {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async function storePinHash(vaultPath, pin) {
|
|
1488
|
+
if (osSecretsDisabled()) {
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
const salt = randomBytes4(16);
|
|
1493
|
+
const hash = scryptSync2(pin, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
|
|
1494
|
+
const value = `${salt.toString("base64")}:${hash.toString("base64")}`;
|
|
1495
|
+
await Bun.secrets.set({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE, value });
|
|
1496
|
+
return true;
|
|
1497
|
+
} catch {
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async function verifyPin(vaultPath, pin) {
|
|
1502
|
+
if (osSecretsDisabled()) {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
const stored = await Bun.secrets.get({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
|
|
1507
|
+
if (!stored)
|
|
1508
|
+
return false;
|
|
1509
|
+
const colonIdx = stored.indexOf(":");
|
|
1510
|
+
if (colonIdx === -1)
|
|
1511
|
+
return false;
|
|
1512
|
+
const salt = Buffer.from(stored.slice(0, colonIdx), "base64");
|
|
1513
|
+
const expectedHash = Buffer.from(stored.slice(colonIdx + 1), "base64");
|
|
1514
|
+
const actualHash = scryptSync2(pin, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
|
|
1515
|
+
return timingSafeEqual2(actualHash, expectedHash);
|
|
1516
|
+
} catch {
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async function hasPinSet(vaultPath) {
|
|
1521
|
+
if (osSecretsDisabled()) {
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
const stored = await Bun.secrets.get({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
|
|
1526
|
+
return stored != null;
|
|
1527
|
+
} catch {
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async function deletePin(vaultPath) {
|
|
1532
|
+
if (osSecretsDisabled()) {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
return await Bun.secrets.delete({ name: vaultPasswordName(vaultPath), service: PIN_SERVICE });
|
|
1537
|
+
} catch {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
var VAULT_PASSWORD_SERVICE = "autho.vault", USER_SECRETS_SERVICE = "autho", PIN_SERVICE = "autho.pin";
|
|
1542
|
+
var init_os_secrets = () => {};
|
|
1543
|
+
|
|
1129
1544
|
// apps/cli/src/tui.tsx
|
|
1130
1545
|
var exports_tui = {};
|
|
1131
1546
|
__export(exports_tui, {
|
|
@@ -1240,28 +1655,33 @@ function useToast() {
|
|
|
1240
1655
|
}, []);
|
|
1241
1656
|
return { toast, show };
|
|
1242
1657
|
}
|
|
1243
|
-
function
|
|
1658
|
+
function tryUnlockWithPassword(vaultPath, password) {
|
|
1659
|
+
try {
|
|
1660
|
+
return VaultService.unlock(vaultPath, { password });
|
|
1661
|
+
} catch {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
function tryUnlockWithRecovery(vaultPath, token) {
|
|
1666
|
+
try {
|
|
1667
|
+
return VaultService.unlock(vaultPath, { password: "", recovery: token });
|
|
1668
|
+
} catch {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
function PasswordMethod({ onSubmit, vaultPath: _vaultPath }) {
|
|
1244
1673
|
const [password, setPassword] = useState("");
|
|
1245
|
-
const [
|
|
1246
|
-
const [unlocking, setUnlocking] = useState(false);
|
|
1674
|
+
const [submitting, setSubmitting] = useState(false);
|
|
1247
1675
|
useKeyboard((key) => {
|
|
1248
1676
|
if (key.eventType !== "press" && key.eventType !== "repeat")
|
|
1249
1677
|
return;
|
|
1250
|
-
if (
|
|
1678
|
+
if (submitting)
|
|
1251
1679
|
return;
|
|
1252
1680
|
if (key.name === "enter" || key.name === "return") {
|
|
1253
1681
|
if (!password)
|
|
1254
1682
|
return;
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
try {
|
|
1258
|
-
const session = VaultService.unlock(vaultPath, password);
|
|
1259
|
-
onUnlock(session);
|
|
1260
|
-
} catch {
|
|
1261
|
-
setError("Wrong password");
|
|
1262
|
-
setPassword("");
|
|
1263
|
-
setUnlocking(false);
|
|
1264
|
-
}
|
|
1683
|
+
setSubmitting(true);
|
|
1684
|
+
onSubmit(password);
|
|
1265
1685
|
return;
|
|
1266
1686
|
}
|
|
1267
1687
|
if (key.name === "backspace") {
|
|
@@ -1274,6 +1694,284 @@ function PasswordScreen({ onUnlock, vaultPath }) {
|
|
|
1274
1694
|
if (char && char.length === 1 && char.charCodeAt(0) >= 32)
|
|
1275
1695
|
setPassword((p) => p + char);
|
|
1276
1696
|
});
|
|
1697
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
1698
|
+
flexDirection: "row",
|
|
1699
|
+
gap: 1,
|
|
1700
|
+
marginTop: 1,
|
|
1701
|
+
children: [
|
|
1702
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
1703
|
+
fg: "#AAAAAA",
|
|
1704
|
+
children: "Password "
|
|
1705
|
+
}, undefined, false, undefined, this),
|
|
1706
|
+
/* @__PURE__ */ jsxDEV("box", {
|
|
1707
|
+
backgroundColor: "#111111",
|
|
1708
|
+
flexGrow: 1,
|
|
1709
|
+
paddingX: 1,
|
|
1710
|
+
height: 1,
|
|
1711
|
+
children: /* @__PURE__ */ jsxDEV("text", {
|
|
1712
|
+
fg: "#FFD700",
|
|
1713
|
+
children: password ? "*".repeat(password.length) : ""
|
|
1714
|
+
}, undefined, false, undefined, this)
|
|
1715
|
+
}, undefined, false, undefined, this),
|
|
1716
|
+
submitting ? /* @__PURE__ */ jsxDEV("text", {
|
|
1717
|
+
fg: "#FFD700",
|
|
1718
|
+
children: " ..."
|
|
1719
|
+
}, undefined, false, undefined, this) : null
|
|
1720
|
+
]
|
|
1721
|
+
}, undefined, true, undefined, this);
|
|
1722
|
+
}
|
|
1723
|
+
function PinMethod({ vaultPath, password, onVerified, onError }) {
|
|
1724
|
+
const [pin, setPin] = useState("");
|
|
1725
|
+
const [verifying, setVerifying] = useState(false);
|
|
1726
|
+
useKeyboard((key) => {
|
|
1727
|
+
if (key.eventType !== "press" && key.eventType !== "repeat")
|
|
1728
|
+
return;
|
|
1729
|
+
if (verifying)
|
|
1730
|
+
return;
|
|
1731
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1732
|
+
if (!pin)
|
|
1733
|
+
return;
|
|
1734
|
+
setVerifying(true);
|
|
1735
|
+
verifyPin(vaultPath, pin).then((ok) => {
|
|
1736
|
+
if (ok) {
|
|
1737
|
+
onVerified(password);
|
|
1738
|
+
} else {
|
|
1739
|
+
onError("Wrong PIN");
|
|
1740
|
+
setPin("");
|
|
1741
|
+
setVerifying(false);
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
if (key.name === "backspace") {
|
|
1747
|
+
setPin((p) => p.slice(0, -1));
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if (key.ctrl || key.meta || key.name === "escape" || key.name === "tab" || key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right" || key.name === "home" || key.name === "end" || key.name === "delete" || key.name === "insert" || key.name === "pageup" || key.name === "pagedown" || key.name.startsWith("f") && /^f\d+$/.test(key.name))
|
|
1751
|
+
return;
|
|
1752
|
+
const char = key.sequence;
|
|
1753
|
+
if (char && char.length === 1 && char.charCodeAt(0) >= 32)
|
|
1754
|
+
setPin((p) => p + char);
|
|
1755
|
+
});
|
|
1756
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
1757
|
+
flexDirection: "row",
|
|
1758
|
+
gap: 1,
|
|
1759
|
+
marginTop: 1,
|
|
1760
|
+
children: [
|
|
1761
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
1762
|
+
fg: "#AAAAAA",
|
|
1763
|
+
children: "PIN "
|
|
1764
|
+
}, undefined, false, undefined, this),
|
|
1765
|
+
/* @__PURE__ */ jsxDEV("box", {
|
|
1766
|
+
backgroundColor: "#111111",
|
|
1767
|
+
flexGrow: 1,
|
|
1768
|
+
paddingX: 1,
|
|
1769
|
+
height: 1,
|
|
1770
|
+
children: /* @__PURE__ */ jsxDEV("text", {
|
|
1771
|
+
fg: "#FFD700",
|
|
1772
|
+
children: pin ? "*".repeat(pin.length) : ""
|
|
1773
|
+
}, undefined, false, undefined, this)
|
|
1774
|
+
}, undefined, false, undefined, this),
|
|
1775
|
+
verifying ? /* @__PURE__ */ jsxDEV("text", {
|
|
1776
|
+
fg: "#FFD700",
|
|
1777
|
+
children: " ..."
|
|
1778
|
+
}, undefined, false, undefined, this) : null
|
|
1779
|
+
]
|
|
1780
|
+
}, undefined, true, undefined, this);
|
|
1781
|
+
}
|
|
1782
|
+
function TotpMethod({ onSubmit, onError: _onError }) {
|
|
1783
|
+
const [code, setCode] = useState("");
|
|
1784
|
+
const [remaining, setRemaining] = useState(0);
|
|
1785
|
+
useEffect(() => {
|
|
1786
|
+
const update = () => {
|
|
1787
|
+
const secs = Math.ceil((30000 - Date.now() % 30000) / 1000);
|
|
1788
|
+
setRemaining(secs);
|
|
1789
|
+
};
|
|
1790
|
+
update();
|
|
1791
|
+
const interval = setInterval(update, 1000);
|
|
1792
|
+
return () => clearInterval(interval);
|
|
1793
|
+
}, []);
|
|
1794
|
+
useKeyboard((key) => {
|
|
1795
|
+
if (key.eventType !== "press" && key.eventType !== "repeat")
|
|
1796
|
+
return;
|
|
1797
|
+
if (key.name === "enter" || key.name === "return") {
|
|
1798
|
+
if (code.length !== 6)
|
|
1799
|
+
return;
|
|
1800
|
+
onSubmit(code);
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (key.name === "backspace") {
|
|
1804
|
+
setCode((c) => c.slice(0, -1));
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (key.ctrl || key.meta || key.name === "escape" || key.name === "tab" || key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right" || key.name === "home" || key.name === "end" || key.name === "delete" || key.name === "insert" || key.name === "pageup" || key.name === "pagedown" || key.name.startsWith("f") && /^f\d+$/.test(key.name))
|
|
1808
|
+
return;
|
|
1809
|
+
const char = key.sequence;
|
|
1810
|
+
if (char && /^\d$/.test(char) && code.length < 6) {
|
|
1811
|
+
const next = code + char;
|
|
1812
|
+
setCode(next);
|
|
1813
|
+
if (next.length === 6) {
|
|
1814
|
+
onSubmit(next);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
1819
|
+
flexDirection: "column",
|
|
1820
|
+
gap: 1,
|
|
1821
|
+
marginTop: 1,
|
|
1822
|
+
children: [
|
|
1823
|
+
/* @__PURE__ */ jsxDEV("box", {
|
|
1824
|
+
flexDirection: "row",
|
|
1825
|
+
gap: 1,
|
|
1826
|
+
children: [
|
|
1827
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
1828
|
+
fg: "#AAAAAA",
|
|
1829
|
+
children: "Auth code "
|
|
1830
|
+
}, undefined, false, undefined, this),
|
|
1831
|
+
/* @__PURE__ */ jsxDEV("box", {
|
|
1832
|
+
backgroundColor: "#111111",
|
|
1833
|
+
flexGrow: 1,
|
|
1834
|
+
paddingX: 1,
|
|
1835
|
+
height: 1,
|
|
1836
|
+
children: /* @__PURE__ */ jsxDEV("text", {
|
|
1837
|
+
fg: "#FFD700",
|
|
1838
|
+
children: code || "_".repeat(6)
|
|
1839
|
+
}, undefined, false, undefined, this)
|
|
1840
|
+
}, undefined, false, undefined, this),
|
|
1841
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
1842
|
+
fg: remaining <= 5 ? "#FF4444" : "#555555",
|
|
1843
|
+
children: [
|
|
1844
|
+
" ",
|
|
1845
|
+
remaining,
|
|
1846
|
+
"s"
|
|
1847
|
+
]
|
|
1848
|
+
}, undefined, true, undefined, this)
|
|
1849
|
+
]
|
|
1850
|
+
}, undefined, true, undefined, this),
|
|
1851
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
1852
|
+
fg: "#555555",
|
|
1853
|
+
children: "Enter 6-digit authenticator code"
|
|
1854
|
+
}, undefined, false, undefined, this)
|
|
1855
|
+
]
|
|
1856
|
+
}, undefined, true, undefined, this);
|
|
1857
|
+
}
|
|
1858
|
+
function UnlockScreen({ onUnlock, vaultPath }) {
|
|
1859
|
+
const [step, setStep] = useState("loading");
|
|
1860
|
+
const [error, setError] = useState("");
|
|
1861
|
+
const [password, setPasswordState] = useState("");
|
|
1862
|
+
const [pinNeeded, setPinNeeded] = useState(false);
|
|
1863
|
+
const [totpNeeded, setTotpNeeded] = useState(false);
|
|
1864
|
+
useEffect(() => {
|
|
1865
|
+
let cancelled = false;
|
|
1866
|
+
(async () => {
|
|
1867
|
+
const recoveryFile = process.env.AUTHO_RECOVERY_FILE;
|
|
1868
|
+
if (recoveryFile) {
|
|
1869
|
+
try {
|
|
1870
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
1871
|
+
const content = readFileSync4(recoveryFile, "utf8");
|
|
1872
|
+
const lines = content.split(`
|
|
1873
|
+
`);
|
|
1874
|
+
const idx = lines.findIndex((l) => l.trim() === "RECOVERY TOKEN:");
|
|
1875
|
+
if (idx !== -1) {
|
|
1876
|
+
const rawToken = lines.slice(idx + 1).find((l) => l.trim() !== "") ?? "";
|
|
1877
|
+
const token = rawToken.replace(/-/g, "").toLowerCase();
|
|
1878
|
+
if (token) {
|
|
1879
|
+
const session = tryUnlockWithRecovery(vaultPath, token);
|
|
1880
|
+
if (!cancelled && session) {
|
|
1881
|
+
onUnlock(session);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (!cancelled)
|
|
1885
|
+
setError("Recovery file failed \u2014 enter password instead");
|
|
1886
|
+
} else {
|
|
1887
|
+
if (!cancelled)
|
|
1888
|
+
setError("Recovery file is malformed \u2014 enter password instead");
|
|
1889
|
+
}
|
|
1890
|
+
} else {
|
|
1891
|
+
if (!cancelled)
|
|
1892
|
+
setError("Recovery file is malformed \u2014 enter password instead");
|
|
1893
|
+
}
|
|
1894
|
+
} catch {
|
|
1895
|
+
if (!cancelled)
|
|
1896
|
+
setError("Recovery file could not be read \u2014 enter password instead");
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const pinSet = await hasPinSet(vaultPath);
|
|
1900
|
+
if (!cancelled)
|
|
1901
|
+
setPinNeeded(pinSet);
|
|
1902
|
+
const authConfig = VaultService.getAuthConfig(vaultPath);
|
|
1903
|
+
if (!cancelled)
|
|
1904
|
+
setTotpNeeded(authConfig?.totp !== undefined);
|
|
1905
|
+
if (!pinSet) {
|
|
1906
|
+
const pw = await loadVaultPassword(vaultPath);
|
|
1907
|
+
if (!cancelled && pw && !authConfig?.totp) {
|
|
1908
|
+
const session = tryUnlockWithPassword(vaultPath, pw);
|
|
1909
|
+
if (!cancelled && session) {
|
|
1910
|
+
onUnlock(session);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (!cancelled && pw) {
|
|
1915
|
+
setPasswordState(pw);
|
|
1916
|
+
if (!cancelled)
|
|
1917
|
+
setStep(authConfig?.totp ? "totp" : "password");
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (!cancelled)
|
|
1922
|
+
setStep("password");
|
|
1923
|
+
})();
|
|
1924
|
+
return () => {
|
|
1925
|
+
cancelled = true;
|
|
1926
|
+
};
|
|
1927
|
+
}, [vaultPath, onUnlock]);
|
|
1928
|
+
const doUnlock = useCallback((pw, totp) => {
|
|
1929
|
+
const creds = { password: pw, totp };
|
|
1930
|
+
try {
|
|
1931
|
+
const session = VaultService.unlock(vaultPath, creds);
|
|
1932
|
+
onUnlock(session);
|
|
1933
|
+
} catch (e) {
|
|
1934
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1935
|
+
setStep("password");
|
|
1936
|
+
setPasswordState("");
|
|
1937
|
+
}
|
|
1938
|
+
}, [vaultPath, onUnlock]);
|
|
1939
|
+
const handlePasswordSubmit = useCallback((pw) => {
|
|
1940
|
+
setPasswordState(pw);
|
|
1941
|
+
if (pinNeeded) {
|
|
1942
|
+
setStep("pin");
|
|
1943
|
+
} else if (totpNeeded) {
|
|
1944
|
+
setStep("totp");
|
|
1945
|
+
} else {
|
|
1946
|
+
setStep("unlocking");
|
|
1947
|
+
doUnlock(pw, undefined);
|
|
1948
|
+
}
|
|
1949
|
+
}, [pinNeeded, totpNeeded, doUnlock]);
|
|
1950
|
+
const handlePinVerified = useCallback((pw) => {
|
|
1951
|
+
if (totpNeeded) {
|
|
1952
|
+
setStep("totp");
|
|
1953
|
+
} else {
|
|
1954
|
+
setStep("unlocking");
|
|
1955
|
+
doUnlock(pw, undefined);
|
|
1956
|
+
}
|
|
1957
|
+
}, [totpNeeded, doUnlock]);
|
|
1958
|
+
const handleTotpSubmit = useCallback((totp) => {
|
|
1959
|
+
setStep("unlocking");
|
|
1960
|
+
doUnlock(password, totp);
|
|
1961
|
+
}, [password, doUnlock]);
|
|
1962
|
+
if (step === "loading" || step === "unlocking") {
|
|
1963
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
1964
|
+
flexDirection: "column",
|
|
1965
|
+
alignItems: "center",
|
|
1966
|
+
justifyContent: "center",
|
|
1967
|
+
width: "100%",
|
|
1968
|
+
height: "100%",
|
|
1969
|
+
children: /* @__PURE__ */ jsxDEV("text", {
|
|
1970
|
+
fg: "#FFD700",
|
|
1971
|
+
children: step === "loading" ? "Unlocking..." : "Verifying..."
|
|
1972
|
+
}, undefined, false, undefined, this)
|
|
1973
|
+
}, undefined, false, undefined, this);
|
|
1974
|
+
}
|
|
1277
1975
|
return /* @__PURE__ */ jsxDEV("box", {
|
|
1278
1976
|
flexDirection: "column",
|
|
1279
1977
|
alignItems: "center",
|
|
@@ -1306,31 +2004,21 @@ function PasswordScreen({ onUnlock, vaultPath }) {
|
|
|
1306
2004
|
children: error
|
|
1307
2005
|
}, undefined, false, undefined, this)
|
|
1308
2006
|
}, undefined, false, undefined, this) : null,
|
|
1309
|
-
/* @__PURE__ */ jsxDEV(
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
fg: "#FFD700",
|
|
1325
|
-
children: password ? "*".repeat(password.length) : ""
|
|
1326
|
-
}, undefined, false, undefined, this)
|
|
1327
|
-
}, undefined, false, undefined, this)
|
|
1328
|
-
]
|
|
1329
|
-
}, undefined, true, undefined, this),
|
|
1330
|
-
unlocking ? /* @__PURE__ */ jsxDEV("text", {
|
|
1331
|
-
fg: "#FFD700",
|
|
1332
|
-
children: "Unlocking..."
|
|
1333
|
-
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("text", {
|
|
2007
|
+
step === "password" && /* @__PURE__ */ jsxDEV(PasswordMethod, {
|
|
2008
|
+
vaultPath,
|
|
2009
|
+
onSubmit: handlePasswordSubmit
|
|
2010
|
+
}, undefined, false, undefined, this),
|
|
2011
|
+
step === "pin" && /* @__PURE__ */ jsxDEV(PinMethod, {
|
|
2012
|
+
vaultPath,
|
|
2013
|
+
password,
|
|
2014
|
+
onVerified: handlePinVerified,
|
|
2015
|
+
onError: setError
|
|
2016
|
+
}, undefined, false, undefined, this),
|
|
2017
|
+
step === "totp" && /* @__PURE__ */ jsxDEV(TotpMethod, {
|
|
2018
|
+
onSubmit: handleTotpSubmit,
|
|
2019
|
+
onError: setError
|
|
2020
|
+
}, undefined, false, undefined, this),
|
|
2021
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
1334
2022
|
fg: "#444444",
|
|
1335
2023
|
children: "Enter to unlock | Ctrl+C to exit"
|
|
1336
2024
|
}, undefined, false, undefined, this)
|
|
@@ -2322,7 +3010,7 @@ function EditScreen({ session, secret, onDone, onBack }) {
|
|
|
2322
3010
|
}, undefined, true, undefined, this);
|
|
2323
3011
|
}
|
|
2324
3012
|
function App({ vaultPath }) {
|
|
2325
|
-
const [screen, setScreen] = useState("
|
|
3013
|
+
const [screen, setScreen] = useState("unlock");
|
|
2326
3014
|
const [session, setSession] = useState(null);
|
|
2327
3015
|
const [selectedSecret, setSelectedSecret] = useState(null);
|
|
2328
3016
|
const { toast, show: showToast } = useToast();
|
|
@@ -2336,8 +3024,8 @@ function App({ vaultPath }) {
|
|
|
2336
3024
|
goHome();
|
|
2337
3025
|
showToast(msg, msg.startsWith("Error:") ? "error" : "success");
|
|
2338
3026
|
}, [goHome, showToast]);
|
|
2339
|
-
if (screen === "
|
|
2340
|
-
return /* @__PURE__ */ jsxDEV(
|
|
3027
|
+
if (screen === "unlock") {
|
|
3028
|
+
return /* @__PURE__ */ jsxDEV(UnlockScreen, {
|
|
2341
3029
|
vaultPath,
|
|
2342
3030
|
onUnlock: (s) => {
|
|
2343
3031
|
setSession(s);
|
|
@@ -2423,6 +3111,7 @@ async function runTui(vaultPath) {
|
|
|
2423
3111
|
var EDIT_FIELDS;
|
|
2424
3112
|
var init_tui = __esm(() => {
|
|
2425
3113
|
init_src3();
|
|
3114
|
+
init_os_secrets();
|
|
2426
3115
|
EDIT_FIELDS = [
|
|
2427
3116
|
{ key: "name", label: "Name", forTypes: ["password", "note", "otp"] },
|
|
2428
3117
|
{ key: "value", label: "Value", forTypes: ["password", "note", "otp"] },
|
|
@@ -2434,11 +3123,30 @@ var init_tui = __esm(() => {
|
|
|
2434
3123
|
|
|
2435
3124
|
// apps/cli/src/index.ts
|
|
2436
3125
|
import { spawn } from "child_process";
|
|
2437
|
-
import { existsSync as existsSync4 } from "fs";
|
|
2438
|
-
import { createInterface } from "readline/promises";
|
|
2439
|
-
import { resolve as
|
|
3126
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
3127
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
3128
|
+
import { resolve as resolve4 } from "path";
|
|
2440
3129
|
|
|
2441
3130
|
// apps/cli/src/password.ts
|
|
3131
|
+
import { createInterface } from "readline/promises";
|
|
3132
|
+
async function readLine(prompt) {
|
|
3133
|
+
if (!process.stdin.isTTY) {
|
|
3134
|
+
throw new Error("Cannot read input: stdin is not a TTY");
|
|
3135
|
+
}
|
|
3136
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3137
|
+
try {
|
|
3138
|
+
return await rl.question(prompt);
|
|
3139
|
+
} finally {
|
|
3140
|
+
rl.close();
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
async function confirm(question, defaultYes = true) {
|
|
3144
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
3145
|
+
const answer = (await readLine(`${question} (${hint}) `)).trim().toLowerCase();
|
|
3146
|
+
if (answer === "")
|
|
3147
|
+
return defaultYes;
|
|
3148
|
+
return answer === "y" || answer === "yes";
|
|
3149
|
+
}
|
|
2442
3150
|
async function readPasswordMasked(prompt = "Master password: ") {
|
|
2443
3151
|
if (!process.stdin.isTTY) {
|
|
2444
3152
|
throw new Error("Cannot read password: stdin is not a TTY");
|
|
@@ -2480,9 +3188,8 @@ async function readPasswordMasked(prompt = "Master password: ") {
|
|
|
2480
3188
|
}
|
|
2481
3189
|
return;
|
|
2482
3190
|
}
|
|
2483
|
-
if (code === 27)
|
|
3191
|
+
if (code === 27)
|
|
2484
3192
|
return;
|
|
2485
|
-
}
|
|
2486
3193
|
if (code >= 32) {
|
|
2487
3194
|
password += char;
|
|
2488
3195
|
process.stdout.write("*");
|
|
@@ -2861,6 +3568,7 @@ async function daemonStop(options) {
|
|
|
2861
3568
|
|
|
2862
3569
|
// apps/cli/src/index.ts
|
|
2863
3570
|
init_src3();
|
|
3571
|
+
init_os_secrets();
|
|
2864
3572
|
function parseArgs(argv) {
|
|
2865
3573
|
const dashDashIndex = argv.indexOf("--");
|
|
2866
3574
|
const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
|
|
@@ -2937,7 +3645,7 @@ function output(value, jsonMode = false) {
|
|
|
2937
3645
|
console.log(value);
|
|
2938
3646
|
}
|
|
2939
3647
|
function absolutePath(path) {
|
|
2940
|
-
return
|
|
3648
|
+
return resolve4(path);
|
|
2941
3649
|
}
|
|
2942
3650
|
function buildSecretMetadata(args) {
|
|
2943
3651
|
return Object.fromEntries(Object.entries({
|
|
@@ -2957,7 +3665,7 @@ async function readBufferedStdin() {
|
|
|
2957
3665
|
}
|
|
2958
3666
|
async function createPromptAdapter() {
|
|
2959
3667
|
if (process.stdin.isTTY) {
|
|
2960
|
-
const rl =
|
|
3668
|
+
const rl = createInterface2({
|
|
2961
3669
|
input: process.stdin,
|
|
2962
3670
|
output: process.stdout
|
|
2963
3671
|
});
|
|
@@ -3049,6 +3757,128 @@ async function runPromptMode(vaultPath, initialPassword) {
|
|
|
3049
3757
|
prompt.close();
|
|
3050
3758
|
}
|
|
3051
3759
|
}
|
|
3760
|
+
async function resolveUnlockCredentials(vaultPath, args, existingPassword) {
|
|
3761
|
+
let password = existingPassword ?? getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
|
|
3762
|
+
if (!password) {
|
|
3763
|
+
password = await loadVaultPassword(vaultPath) ?? undefined;
|
|
3764
|
+
}
|
|
3765
|
+
if (!password && process.stdin.isTTY) {
|
|
3766
|
+
password = await readPasswordMasked("Master password: ");
|
|
3767
|
+
}
|
|
3768
|
+
const creds = { password: required(password, "--password") };
|
|
3769
|
+
if (await hasPinSet(vaultPath)) {
|
|
3770
|
+
const pin = process.env.AUTHO_PIN ?? getString(args, "pin") ?? (process.stdin.isTTY ? await readPasswordMasked("PIN: ") : undefined);
|
|
3771
|
+
if (!pin)
|
|
3772
|
+
throw new Error("PIN is set on this vault \u2014 provide it with AUTHO_PIN, --pin, or interactively");
|
|
3773
|
+
const ok = await verifyPin(vaultPath, pin);
|
|
3774
|
+
if (!ok)
|
|
3775
|
+
throw new Error("Wrong PIN");
|
|
3776
|
+
}
|
|
3777
|
+
const authConfig = VaultService.getAuthConfig(vaultPath);
|
|
3778
|
+
if (authConfig?.totp) {
|
|
3779
|
+
const totp = process.env.AUTHO_TOTP_CODE ?? getString(args, "totp") ?? (process.stdin.isTTY ? await readPasswordMasked("Authenticator code: ") : undefined);
|
|
3780
|
+
if (!totp)
|
|
3781
|
+
throw new Error("TOTP is enabled \u2014 provide a 6-digit code with AUTHO_TOTP_CODE, --totp, or interactively");
|
|
3782
|
+
creds.totp = totp;
|
|
3783
|
+
}
|
|
3784
|
+
return creds;
|
|
3785
|
+
}
|
|
3786
|
+
function osKeychainName() {
|
|
3787
|
+
const p = process.platform;
|
|
3788
|
+
if (p === "darwin")
|
|
3789
|
+
return "macOS Keychain";
|
|
3790
|
+
if (p === "win32")
|
|
3791
|
+
return "Windows Credential Manager";
|
|
3792
|
+
return "Secret Service (libsecret)";
|
|
3793
|
+
}
|
|
3794
|
+
async function runInitWizard(vaultPath, password, existingCreds) {
|
|
3795
|
+
const creds = existingCreds ?? { password };
|
|
3796
|
+
console.log("");
|
|
3797
|
+
if (!osSecretsDisabled()) {
|
|
3798
|
+
const passwordStored = await loadVaultPassword(vaultPath) !== null;
|
|
3799
|
+
if (passwordStored) {
|
|
3800
|
+
console.log(`\x1B[32m\u2713\x1B[0m Master password is saved in ${osKeychainName()}.`);
|
|
3801
|
+
if (await confirm(" Remove it?", false)) {
|
|
3802
|
+
const deleted = await deleteVaultPassword(vaultPath);
|
|
3803
|
+
console.log(deleted ? " Removed." : " Could not remove.");
|
|
3804
|
+
}
|
|
3805
|
+
} else {
|
|
3806
|
+
console.log(`\x1B[33m?\x1B[0m Save master password to ${osKeychainName()}?`);
|
|
3807
|
+
console.log(" This lets all vault commands unlock without prompting on this machine.");
|
|
3808
|
+
if (await confirm(" Save to keychain?")) {
|
|
3809
|
+
const stored = await storeVaultPassword(vaultPath, password);
|
|
3810
|
+
console.log(stored ? " \x1B[32m\u2713\x1B[0m Saved." : " Could not save \u2014 OS secret store may be unavailable.");
|
|
3811
|
+
} else {
|
|
3812
|
+
console.log(" Skipped.");
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
console.log("");
|
|
3816
|
+
}
|
|
3817
|
+
if (!osSecretsDisabled()) {
|
|
3818
|
+
const pinSet = await hasPinSet(vaultPath);
|
|
3819
|
+
if (pinSet) {
|
|
3820
|
+
console.log("\x1B[32m\u2713\x1B[0m PIN is set on this machine.");
|
|
3821
|
+
if (await confirm(" Remove PIN?", false)) {
|
|
3822
|
+
const pin = await readPasswordMasked(" Enter current PIN: ");
|
|
3823
|
+
const ok = await verifyPin(vaultPath, pin);
|
|
3824
|
+
if (ok) {
|
|
3825
|
+
await deletePin(vaultPath);
|
|
3826
|
+
console.log(" Removed.");
|
|
3827
|
+
} else {
|
|
3828
|
+
console.log(" Wrong PIN, not removed.");
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
} else {
|
|
3832
|
+
if (await confirm("Set up a PIN for quick unlock on this machine?", false)) {
|
|
3833
|
+
const newPin = await readPasswordMasked(" New PIN: ");
|
|
3834
|
+
if (!newPin) {
|
|
3835
|
+
console.log(" PIN cannot be empty, skipped.");
|
|
3836
|
+
} else {
|
|
3837
|
+
const confirmPin = await readPasswordMasked(" Confirm PIN: ");
|
|
3838
|
+
if (newPin !== confirmPin) {
|
|
3839
|
+
console.log(" PINs do not match, skipped.");
|
|
3840
|
+
} else {
|
|
3841
|
+
await storePinHash(vaultPath, newPin);
|
|
3842
|
+
console.log(" \x1B[32m\u2713\x1B[0m PIN set.");
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
console.log("");
|
|
3848
|
+
}
|
|
3849
|
+
const authConfig = VaultService.getAuthConfig(vaultPath);
|
|
3850
|
+
const totpEnabled = authConfig?.totp !== undefined;
|
|
3851
|
+
if (totpEnabled) {
|
|
3852
|
+
console.log("\x1B[32m\u2713\x1B[0m TOTP is enabled (authenticator app required to unlock).");
|
|
3853
|
+
if (await confirm(" Disable TOTP?", false)) {
|
|
3854
|
+
const code = await readPasswordMasked(" Enter current TOTP code: ");
|
|
3855
|
+
try {
|
|
3856
|
+
VaultService.removeTotp(vaultPath, { ...creds, totp: code });
|
|
3857
|
+
console.log(" Disabled.");
|
|
3858
|
+
} catch (e) {
|
|
3859
|
+
console.log(` Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
} else {
|
|
3863
|
+
if (await confirm("Enable TOTP (authenticator app verification)?", false)) {
|
|
3864
|
+
const { secret, uri } = VaultService.setupTotp(vaultPath);
|
|
3865
|
+
console.log("");
|
|
3866
|
+
console.log(` Secret: ${secret}`);
|
|
3867
|
+
console.log(` URI: ${uri}`);
|
|
3868
|
+
console.log("");
|
|
3869
|
+
console.log(" Add this to your authenticator app, then enter the 6-digit code.");
|
|
3870
|
+
const code = await readPasswordMasked(" Code: ");
|
|
3871
|
+
try {
|
|
3872
|
+
VaultService.enableTotp(vaultPath, creds, secret, code);
|
|
3873
|
+
console.log(" \x1B[32m\u2713\x1B[0m TOTP enabled.");
|
|
3874
|
+
} catch (e) {
|
|
3875
|
+
console.log(` Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
console.log("");
|
|
3880
|
+
console.log("Setup complete. Run \x1B[1mautho init\x1B[0m anytime to change these settings.");
|
|
3881
|
+
}
|
|
3052
3882
|
async function runWebServer(vaultPath, args) {
|
|
3053
3883
|
const commandArgs = [
|
|
3054
3884
|
"run",
|
|
@@ -3118,15 +3948,32 @@ function help() {
|
|
|
3118
3948
|
" files encrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
|
|
3119
3949
|
" files decrypt --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
|
|
3120
3950
|
" audit list [--limit <number>] [--vault <path>] [--json]",
|
|
3951
|
+
" recovery generate --output <path> [--vault <path>] [--json]",
|
|
3952
|
+
" recovery revoke [--vault <path>] [--json]",
|
|
3953
|
+
" unlock --recovery-file <path> [--vault <path>] [--json]",
|
|
3954
|
+
" os-secrets set --name <name> [--value <value>] [--json]",
|
|
3955
|
+
" os-secrets get --name <name> [--json]",
|
|
3956
|
+
" os-secrets delete --name <name> [--json]",
|
|
3121
3957
|
"",
|
|
3122
3958
|
"Authentication:",
|
|
3123
3959
|
" When running interactively (TTY), you will be securely prompted for your",
|
|
3124
3960
|
" master password with masked input (no --password flag needed).",
|
|
3125
3961
|
"",
|
|
3126
3962
|
" For automation and coding agents, use one of:",
|
|
3127
|
-
"
|
|
3963
|
+
" autho init Setup wizard \u2014 choose to save password in OS keychain, set PIN, enable TOTP",
|
|
3964
|
+
" AUTHO_MASTER_PASSWORD=<value> Environment variable (master password)",
|
|
3965
|
+
" AUTHO_PIN=<value> Environment variable (PIN, if set on this machine)",
|
|
3966
|
+
" AUTHO_TOTP_CODE=<value> Environment variable (TOTP code, if enabled)",
|
|
3128
3967
|
" --password <value> CLI flag (visible in shell history - avoid!)",
|
|
3129
3968
|
"",
|
|
3969
|
+
" Native OS secret store support (via Bun.secrets):",
|
|
3970
|
+
" macOS \u2192 Keychain Services",
|
|
3971
|
+
" Linux \u2192 libsecret / GNOME Keyring / KWallet",
|
|
3972
|
+
" Windows \u2192 Windows Credential Manager",
|
|
3973
|
+
"",
|
|
3974
|
+
" The OS secret store is checked automatically before prompting.",
|
|
3975
|
+
" Set AUTHO_DISABLE_OS_SECRETS=1 to opt out.",
|
|
3976
|
+
"",
|
|
3130
3977
|
"Notes:",
|
|
3131
3978
|
" Running `autho` with no arguments opens the interactive TUI.",
|
|
3132
3979
|
" The default vault path is ~/.autho/vault.db (override with AUTHO_HOME).",
|
|
@@ -3145,6 +3992,9 @@ async function main() {
|
|
|
3145
3992
|
const fallbackProjectFile = defaultProjectFilePath();
|
|
3146
3993
|
const projectFile = explicitProjectFile ?? (existsSync4(fallbackProjectFile) ? fallbackProjectFile : undefined);
|
|
3147
3994
|
let password = getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
|
|
3995
|
+
if (!password) {
|
|
3996
|
+
password = await loadVaultPassword(vaultPath) ?? undefined;
|
|
3997
|
+
}
|
|
3148
3998
|
if (!scope) {
|
|
3149
3999
|
if (process.stdin.isTTY) {
|
|
3150
4000
|
const { runTui: runTui2 } = await Promise.resolve().then(() => (init_tui(), exports_tui));
|
|
@@ -3164,7 +4014,6 @@ async function main() {
|
|
|
3164
4014
|
}
|
|
3165
4015
|
if (!password && process.stdin.isTTY) {
|
|
3166
4016
|
const needsPassword = [
|
|
3167
|
-
"init",
|
|
3168
4017
|
"secrets",
|
|
3169
4018
|
"otp",
|
|
3170
4019
|
"lease",
|
|
@@ -3173,7 +4022,9 @@ async function main() {
|
|
|
3173
4022
|
"file",
|
|
3174
4023
|
"files",
|
|
3175
4024
|
"audit",
|
|
3176
|
-
"import"
|
|
4025
|
+
"import",
|
|
4026
|
+
"recovery",
|
|
4027
|
+
"unlock"
|
|
3177
4028
|
].includes(scope);
|
|
3178
4029
|
const daemonNeedsPassword = scope === "daemon" && action === "unlock";
|
|
3179
4030
|
if (needsPassword || daemonNeedsPassword) {
|
|
@@ -3181,7 +4032,21 @@ async function main() {
|
|
|
3181
4032
|
}
|
|
3182
4033
|
}
|
|
3183
4034
|
if (scope === "init") {
|
|
3184
|
-
|
|
4035
|
+
const existingStatus = VaultService.status(vaultPath);
|
|
4036
|
+
if (!existingStatus.initialized) {
|
|
4037
|
+
const pw = required(password, "--password");
|
|
4038
|
+
output(VaultService.initialize(vaultPath, pw), jsonMode);
|
|
4039
|
+
if (process.stdin.isTTY && !jsonMode) {
|
|
4040
|
+
await runInitWizard(vaultPath, pw);
|
|
4041
|
+
}
|
|
4042
|
+
} else {
|
|
4043
|
+
const creds2 = await resolveUnlockCredentials(vaultPath, args, password);
|
|
4044
|
+
if (process.stdin.isTTY && !jsonMode) {
|
|
4045
|
+
await runInitWizard(vaultPath, creds2.password, creds2);
|
|
4046
|
+
} else {
|
|
4047
|
+
console.log("Vault already initialized. Use interactive mode (TTY) to reconfigure.");
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
3185
4050
|
return;
|
|
3186
4051
|
}
|
|
3187
4052
|
if (scope === "status") {
|
|
@@ -3199,6 +4064,31 @@ async function main() {
|
|
|
3199
4064
|
}), jsonMode);
|
|
3200
4065
|
return;
|
|
3201
4066
|
}
|
|
4067
|
+
if (scope === "os-secrets" && action === "set") {
|
|
4068
|
+
const name = required(getString(args, "name"), "--name");
|
|
4069
|
+
const value = getString(args, "value") ?? await readPasswordMasked(`Value for "${name}": `);
|
|
4070
|
+
const stored = await setOsSecret(name, value);
|
|
4071
|
+
if (!stored) {
|
|
4072
|
+
throw new Error("OS secret store is unavailable on this system (try AUTHO_DISABLE_OS_SECRETS=1 to confirm, or check that a secret service daemon is running on Linux)");
|
|
4073
|
+
}
|
|
4074
|
+
output({ name, stored: true }, jsonMode);
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
if (scope === "os-secrets" && action === "get") {
|
|
4078
|
+
const name = required(getString(args, "name"), "--name");
|
|
4079
|
+
const value = await getOsSecret(name);
|
|
4080
|
+
if (value === null) {
|
|
4081
|
+
throw new Error(`Secret "${name}" not found in OS secret store`);
|
|
4082
|
+
}
|
|
4083
|
+
output({ name, value }, jsonMode);
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
if (scope === "os-secrets" && action === "delete") {
|
|
4087
|
+
const name = required(getString(args, "name"), "--name");
|
|
4088
|
+
const deleted = await deleteOsSecret(name);
|
|
4089
|
+
output({ deleted, name }, jsonMode);
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
3202
4092
|
if (scope === "web" && action === "serve") {
|
|
3203
4093
|
await runWebServer(vaultPath, args);
|
|
3204
4094
|
return;
|
|
@@ -3256,7 +4146,44 @@ async function main() {
|
|
|
3256
4146
|
process.stderr.write(result.stderr);
|
|
3257
4147
|
process.exit(result.exitCode);
|
|
3258
4148
|
}
|
|
3259
|
-
|
|
4149
|
+
if (scope === "recovery" && action === "generate") {
|
|
4150
|
+
const outputPath = absolutePath(required(getString(args, "output"), "--output"));
|
|
4151
|
+
const creds2 = await resolveUnlockCredentials(vaultPath, args, password);
|
|
4152
|
+
const { fileContent } = VaultService.generateRecovery(vaultPath, creds2);
|
|
4153
|
+
writeFileSync2(outputPath, fileContent, { encoding: "utf8", mode: 384 });
|
|
4154
|
+
if (!jsonMode) {
|
|
4155
|
+
console.log(`Recovery file written to ${outputPath}`);
|
|
4156
|
+
console.log("WARNING: Anyone with this file can open your vault. Store it offline.");
|
|
4157
|
+
} else {
|
|
4158
|
+
output({ outputPath, written: true }, jsonMode);
|
|
4159
|
+
}
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4162
|
+
if (scope === "recovery" && action === "revoke") {
|
|
4163
|
+
const creds2 = await resolveUnlockCredentials(vaultPath, args, password);
|
|
4164
|
+
VaultService.revokeRecovery(vaultPath, creds2);
|
|
4165
|
+
output({ revoked: true }, jsonMode);
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
if (scope === "unlock" && getString(args, "recovery-file")) {
|
|
4169
|
+
const recoveryFilePath = absolutePath(getString(args, "recovery-file"));
|
|
4170
|
+
const content = readFileSync4(recoveryFilePath, "utf8");
|
|
4171
|
+
const lines = content.split(`
|
|
4172
|
+
`);
|
|
4173
|
+
const tokenLineIdx = lines.findIndex((l) => l.trim() === "RECOVERY TOKEN:");
|
|
4174
|
+
if (tokenLineIdx === -1)
|
|
4175
|
+
throw new Error("Invalid recovery file format");
|
|
4176
|
+
const rawTokenLine = lines.slice(tokenLineIdx + 1).find((l) => l.trim() !== "") ?? "";
|
|
4177
|
+
const token = rawTokenLine.replace(/-/g, "").toLowerCase();
|
|
4178
|
+
if (!token)
|
|
4179
|
+
throw new Error("Invalid recovery file format: missing token");
|
|
4180
|
+
const recoverySession = VaultService.unlock(vaultPath, { password: "", recovery: token });
|
|
4181
|
+
output({ unlocked: true, vaultPath }, jsonMode);
|
|
4182
|
+
recoverySession.close();
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
const creds = await resolveUnlockCredentials(vaultPath, args, password);
|
|
4186
|
+
const session = VaultService.unlock(vaultPath, creds);
|
|
3260
4187
|
try {
|
|
3261
4188
|
if (scope === "import" && action === "legacy") {
|
|
3262
4189
|
output(session.importLegacyFile(absolutePath(required(getString(args, "file"), "--file")), {
|