@lifeaitools/clauth 1.30.2 → 1.30.3

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/cli/recovery.js CHANGED
@@ -1,101 +1,101 @@
1
- import crypto from "crypto";
2
- import fs from "fs";
3
- import os from "os";
4
- import path from "path";
5
- import { getConfOptions } from "./conf-path.js";
6
- import { deriveToken } from "./fingerprint.js";
7
- import * as api from "./api.js";
8
-
9
- const RECOVERY_VERSION = 1;
10
-
11
- function getRecoveryDir() {
12
- const opts = getConfOptions();
13
- if (opts.cwd) return path.join(opts.cwd, "recovery");
14
- return path.join(os.homedir(), ".config", "clauth", "recovery");
15
- }
16
-
17
- function hashValue(value) {
18
- return crypto.createHash("sha256").update(String(value), "utf8").digest("hex");
19
- }
20
-
21
- function deriveRecoveryKey(password, machineHash, salt) {
22
- return crypto.scryptSync(`${password}:${machineHash}`, salt, 32);
23
- }
24
-
25
- function safeServiceName(service) {
26
- return String(service || "unknown").replace(/[^a-zA-Z0-9_-]/g, "_");
27
- }
28
-
29
- export function normalizeCredentialValue(value, { keyType = "", service = "" } = {}) {
30
- if (typeof value !== "string") return value;
31
- const text = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
32
- const looksPem = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/.test(text);
33
- const looksSsh = String(keyType).toLowerCase() === "ssh" || /ssh|pem|private/i.test(service);
34
- if (!looksPem && !looksSsh) return value;
35
- return text.replace(/\n*$/, "\n");
36
- }
37
-
38
- export async function snapshotCredentialBeforeWrite({ password, machineHash, service, logFile }) {
39
- if (!password || !machineHash || !service) return { ok: false, skipped: "missing_context" };
40
-
41
- let current;
42
- try {
43
- const { token, timestamp } = deriveToken(password, machineHash);
44
- current = await api.retrieve(password, machineHash, token, timestamp, service);
45
- } catch (err) {
46
- return { ok: false, skipped: "retrieve_failed", error: err.message };
47
- }
48
-
49
- if (current?.error || current?.value === undefined || current?.value === null) {
50
- return { ok: false, skipped: current?.error || "no_existing_value" };
51
- }
52
-
53
- const value = typeof current.value === "string" ? current.value : JSON.stringify(current.value);
54
- const now = new Date().toISOString();
55
- const salt = crypto.randomBytes(16);
56
- const iv = crypto.randomBytes(12);
57
- const key = deriveRecoveryKey(password, machineHash, salt);
58
- const meta = {
59
- version: RECOVERY_VERSION,
60
- service,
61
- key_type: current.key_type || null,
62
- created_at: now,
63
- value_sha256: hashValue(value),
64
- };
65
-
66
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
67
- cipher.setAAD(Buffer.from(JSON.stringify(meta), "utf8"));
68
- const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
69
- const tag = cipher.getAuthTag();
70
-
71
- const payload = {
72
- ...meta,
73
- kdf: "scrypt",
74
- cipher: "aes-256-gcm",
75
- salt: salt.toString("base64"),
76
- iv: iv.toString("base64"),
77
- tag: tag.toString("base64"),
78
- ciphertext: ciphertext.toString("base64"),
79
- };
80
-
81
- const dir = getRecoveryDir();
82
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
83
- const stamp = now.replace(/[-:.]/g, "").replace("T", "-").replace("Z", "");
84
- const filePath = path.join(dir, `${stamp}-${safeServiceName(service)}.json`);
85
- fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });
86
-
87
- if (logFile) {
88
- try { fs.appendFileSync(logFile, `[${now}] Recovery snapshot written for ${service}: ${filePath}\n`); } catch {}
89
- }
90
- return { ok: true, filePath, value_sha256: meta.value_sha256 };
91
- }
92
-
93
- export async function writeCredentialWithRecovery({ password, machineHash, service, value, logFile, normalize = true }) {
94
- const { token, timestamp } = deriveToken(password, machineHash);
95
- const status = await api.status(password, machineHash, token, timestamp);
96
- const svc = (status.services || []).find(s => String(s.name || "").toLowerCase() === String(service || "").toLowerCase());
97
- const normalizedValue = normalize ? normalizeCredentialValue(value, { keyType: svc?.key_type, service }) : value;
98
- const snapshot = await snapshotCredentialBeforeWrite({ password, machineHash, service, logFile });
99
- const result = await api.write(password, machineHash, token, timestamp, service, normalizedValue);
100
- return { result, snapshot, normalized: normalizedValue !== value };
101
- }
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { getConfOptions } from "./conf-path.js";
6
+ import { deriveToken } from "./fingerprint.js";
7
+ import * as api from "./api.js";
8
+
9
+ const RECOVERY_VERSION = 1;
10
+
11
+ function getRecoveryDir() {
12
+ const opts = getConfOptions();
13
+ if (opts.cwd) return path.join(opts.cwd, "recovery");
14
+ return path.join(os.homedir(), ".config", "clauth", "recovery");
15
+ }
16
+
17
+ function hashValue(value) {
18
+ return crypto.createHash("sha256").update(String(value), "utf8").digest("hex");
19
+ }
20
+
21
+ function deriveRecoveryKey(password, machineHash, salt) {
22
+ return crypto.scryptSync(`${password}:${machineHash}`, salt, 32);
23
+ }
24
+
25
+ function safeServiceName(service) {
26
+ return String(service || "unknown").replace(/[^a-zA-Z0-9_-]/g, "_");
27
+ }
28
+
29
+ export function normalizeCredentialValue(value, { keyType = "", service = "" } = {}) {
30
+ if (typeof value !== "string") return value;
31
+ const text = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
32
+ const looksPem = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/.test(text);
33
+ const looksSsh = String(keyType).toLowerCase() === "ssh" || /ssh|pem|private/i.test(service);
34
+ if (!looksPem && !looksSsh) return value;
35
+ return text.replace(/\n*$/, "\n");
36
+ }
37
+
38
+ export async function snapshotCredentialBeforeWrite({ password, machineHash, service, logFile }) {
39
+ if (!password || !machineHash || !service) return { ok: false, skipped: "missing_context" };
40
+
41
+ let current;
42
+ try {
43
+ const { token, timestamp } = deriveToken(password, machineHash);
44
+ current = await api.retrieve(password, machineHash, token, timestamp, service);
45
+ } catch (err) {
46
+ return { ok: false, skipped: "retrieve_failed", error: err.message };
47
+ }
48
+
49
+ if (current?.error || current?.value === undefined || current?.value === null) {
50
+ return { ok: false, skipped: current?.error || "no_existing_value" };
51
+ }
52
+
53
+ const value = typeof current.value === "string" ? current.value : JSON.stringify(current.value);
54
+ const now = new Date().toISOString();
55
+ const salt = crypto.randomBytes(16);
56
+ const iv = crypto.randomBytes(12);
57
+ const key = deriveRecoveryKey(password, machineHash, salt);
58
+ const meta = {
59
+ version: RECOVERY_VERSION,
60
+ service,
61
+ key_type: current.key_type || null,
62
+ created_at: now,
63
+ value_sha256: hashValue(value),
64
+ };
65
+
66
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
67
+ cipher.setAAD(Buffer.from(JSON.stringify(meta), "utf8"));
68
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
69
+ const tag = cipher.getAuthTag();
70
+
71
+ const payload = {
72
+ ...meta,
73
+ kdf: "scrypt",
74
+ cipher: "aes-256-gcm",
75
+ salt: salt.toString("base64"),
76
+ iv: iv.toString("base64"),
77
+ tag: tag.toString("base64"),
78
+ ciphertext: ciphertext.toString("base64"),
79
+ };
80
+
81
+ const dir = getRecoveryDir();
82
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
83
+ const stamp = now.replace(/[-:.]/g, "").replace("T", "-").replace("Z", "");
84
+ const filePath = path.join(dir, `${stamp}-${safeServiceName(service)}.json`);
85
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });
86
+
87
+ if (logFile) {
88
+ try { fs.appendFileSync(logFile, `[${now}] Recovery snapshot written for ${service}: ${filePath}\n`); } catch {}
89
+ }
90
+ return { ok: true, filePath, value_sha256: meta.value_sha256 };
91
+ }
92
+
93
+ export async function writeCredentialWithRecovery({ password, machineHash, service, value, logFile, normalize = true }) {
94
+ const { token, timestamp } = deriveToken(password, machineHash);
95
+ const status = await api.status(password, machineHash, token, timestamp);
96
+ const svc = (status.services || []).find(s => String(s.name || "").toLowerCase() === String(service || "").toLowerCase());
97
+ const normalizedValue = normalize ? normalizeCredentialValue(value, { keyType: svc?.key_type, service }) : value;
98
+ const snapshot = await snapshotCredentialBeforeWrite({ password, machineHash, service, logFile });
99
+ const result = await api.write(password, machineHash, token, timestamp, service, normalizedValue);
100
+ return { result, snapshot, normalized: normalizedValue !== value };
101
+ }