passwd-sso-cli 0.4.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/dist/commands/agent-decrypt.d.ts +11 -0
- package/dist/commands/agent-decrypt.js +317 -0
- package/dist/commands/agent.d.ts +13 -0
- package/dist/commands/agent.js +116 -0
- package/dist/commands/api-key.d.ts +22 -0
- package/dist/commands/api-key.js +118 -0
- package/dist/commands/decrypt.d.ts +17 -0
- package/dist/commands/decrypt.js +108 -0
- package/dist/commands/env.d.ts +15 -0
- package/dist/commands/env.js +102 -0
- package/dist/commands/export.d.ts +7 -0
- package/dist/commands/export.js +99 -0
- package/dist/commands/generate.d.ts +11 -0
- package/dist/commands/generate.js +45 -0
- package/dist/commands/get.d.ts +8 -0
- package/dist/commands/get.js +73 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +66 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +45 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +97 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +62 -0
- package/dist/commands/totp.d.ts +7 -0
- package/dist/commands/totp.js +57 -0
- package/dist/commands/unlock.d.ts +19 -0
- package/dist/commands/unlock.js +125 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +298 -0
- package/dist/lib/api-client.d.ts +22 -0
- package/dist/lib/api-client.js +145 -0
- package/dist/lib/blocked-keys.d.ts +2 -0
- package/dist/lib/blocked-keys.js +23 -0
- package/dist/lib/clipboard.d.ts +8 -0
- package/dist/lib/clipboard.js +79 -0
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.js +110 -0
- package/dist/lib/crypto-aad.d.ts +5 -0
- package/dist/lib/crypto-aad.js +44 -0
- package/dist/lib/crypto.d.ts +23 -0
- package/dist/lib/crypto.js +148 -0
- package/dist/lib/migrate.d.ts +8 -0
- package/dist/lib/migrate.js +87 -0
- package/dist/lib/openssh-key-parser.d.ts +17 -0
- package/dist/lib/openssh-key-parser.js +273 -0
- package/dist/lib/output.d.ts +10 -0
- package/dist/lib/output.js +36 -0
- package/dist/lib/paths.d.ts +17 -0
- package/dist/lib/paths.js +39 -0
- package/dist/lib/secrets-config.d.ts +31 -0
- package/dist/lib/secrets-config.js +48 -0
- package/dist/lib/ssh-agent-protocol.d.ts +56 -0
- package/dist/lib/ssh-agent-protocol.js +108 -0
- package/dist/lib/ssh-agent-socket.d.ts +20 -0
- package/dist/lib/ssh-agent-socket.js +187 -0
- package/dist/lib/ssh-key-agent.d.ts +54 -0
- package/dist/lib/ssh-key-agent.js +197 -0
- package/dist/lib/totp.d.ts +10 -0
- package/dist/lib/totp.js +31 -0
- package/dist/lib/vault-state.d.ts +15 -0
- package/dist/lib/vault-state.js +37 -0
- package/package.json +56 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `passwd-sso run` — Inject vault secrets into a command's environment.
|
|
3
|
+
*
|
|
4
|
+
* Uses child_process.execFile (NOT shell) for security.
|
|
5
|
+
* Blocks certain dangerous env var names from being overwritten.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { loadSecretsConfig, getPasswordPath } from "../lib/secrets-config.js";
|
|
9
|
+
import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
|
|
10
|
+
import { autoUnlockIfNeeded } from "./unlock.js";
|
|
11
|
+
import { getToken } from "../lib/api-client.js";
|
|
12
|
+
import { decryptData } from "../lib/crypto.js";
|
|
13
|
+
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
14
|
+
import * as output from "../lib/output.js";
|
|
15
|
+
import { BLOCKED_KEYS } from "../lib/blocked-keys.js";
|
|
16
|
+
export async function runCommand(opts) {
|
|
17
|
+
if (opts.command.length === 0) {
|
|
18
|
+
output.error("No command specified. Usage: passwd-sso run -- <command>");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
let config;
|
|
22
|
+
try {
|
|
23
|
+
config = loadSecretsConfig(opts.config);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
output.error(err instanceof Error ? err.message : "Failed to load config.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const useV1 = !!config.apiKey;
|
|
30
|
+
const baseUrl = config.server.replace(/\/$/, "");
|
|
31
|
+
let authHeader;
|
|
32
|
+
if (config.apiKey) {
|
|
33
|
+
authHeader = `Bearer ${config.apiKey}`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const token = await getToken();
|
|
37
|
+
if (!token) {
|
|
38
|
+
output.error("Not logged in. Run `passwd-sso login` first, or set apiKey in config.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
authHeader = `Bearer ${token}`;
|
|
42
|
+
}
|
|
43
|
+
if (!await autoUnlockIfNeeded()) {
|
|
44
|
+
output.error("Vault is not unlocked. Run `passwd-sso unlock` first, or set PSSO_PASSPHRASE.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const encryptionKey = getEncryptionKey();
|
|
48
|
+
const userId = getUserId();
|
|
49
|
+
// Resolve secrets
|
|
50
|
+
const secretEnv = {};
|
|
51
|
+
const entries = Object.entries(config.secrets);
|
|
52
|
+
for (const [envName, mapping] of entries) {
|
|
53
|
+
// Block dangerous keys
|
|
54
|
+
if (BLOCKED_KEYS.has(envName.toUpperCase())) {
|
|
55
|
+
output.error(`Blocked: cannot inject '${envName}' (security restriction)`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const path = getPasswordPath(mapping.entry, useV1);
|
|
59
|
+
const url = `${baseUrl}${path}`;
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
headers: { Authorization: authHeader },
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
output.error(`Failed to fetch entry ${mapping.entry}: HTTP ${res.status}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const data = (await res.json());
|
|
68
|
+
let additionalData;
|
|
69
|
+
if (data.aadVersion && data.aadVersion >= 1 && userId) {
|
|
70
|
+
additionalData = buildPersonalEntryAAD(userId, data.id);
|
|
71
|
+
}
|
|
72
|
+
const decrypted = await decryptData(data.encryptedBlob, encryptionKey, additionalData);
|
|
73
|
+
const blob = JSON.parse(decrypted);
|
|
74
|
+
const value = blob[mapping.field];
|
|
75
|
+
if (value === undefined || value === null) {
|
|
76
|
+
output.error(`Field '${mapping.field}' not found in entry ${mapping.entry}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
secretEnv[envName] = String(value);
|
|
80
|
+
}
|
|
81
|
+
// Execute command with injected env vars (shell-free)
|
|
82
|
+
// Strip PSSO_ credentials from parent env to prevent leakage to child (CWE-214)
|
|
83
|
+
const { PSSO_PASSPHRASE: _pass, PSSO_API_KEY: _key, ...safeParentEnv } = process.env;
|
|
84
|
+
const [cmd, ...args] = opts.command;
|
|
85
|
+
const child = spawn(cmd, args, {
|
|
86
|
+
env: { ...safeParentEnv, ...secretEnv },
|
|
87
|
+
stdio: "inherit",
|
|
88
|
+
});
|
|
89
|
+
child.on("exit", (code) => {
|
|
90
|
+
process.exit(code ?? 1);
|
|
91
|
+
});
|
|
92
|
+
child.on("error", (err) => {
|
|
93
|
+
output.error(`Failed to execute: ${err.message}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `passwd-sso status` — Show connection and vault status.
|
|
3
|
+
*/
|
|
4
|
+
import { apiRequest } from "../lib/api-client.js";
|
|
5
|
+
import { isUnlocked } from "../lib/vault-state.js";
|
|
6
|
+
import { loadConfig } from "../lib/config.js";
|
|
7
|
+
import * as output from "../lib/output.js";
|
|
8
|
+
export async function statusCommand(options = {}) {
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
const unlocked = isUnlocked();
|
|
11
|
+
if (!config.serverUrl) {
|
|
12
|
+
if (options.json) {
|
|
13
|
+
output.json({ server: null, vault: unlocked ? "unlocked" : "locked", connected: false });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log(`Server: (not configured)`);
|
|
17
|
+
console.log(`Vault: ${unlocked ? "Unlocked" : "Locked"}`);
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const res = await apiRequest("/api/vault/status");
|
|
23
|
+
if (res.ok) {
|
|
24
|
+
const vaultSetup = res.data.setupRequired === false;
|
|
25
|
+
if (options.json) {
|
|
26
|
+
output.json({
|
|
27
|
+
server: config.serverUrl,
|
|
28
|
+
vault: unlocked ? "unlocked" : "locked",
|
|
29
|
+
setup: vaultSetup ? "complete" : "required",
|
|
30
|
+
connected: true,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(`Server: ${config.serverUrl}`);
|
|
35
|
+
console.log(`Vault: ${unlocked ? "Unlocked" : "Locked"}`);
|
|
36
|
+
console.log(`Setup: ${vaultSetup ? "Complete" : "Required"}`);
|
|
37
|
+
output.success("Connected");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
if (options.json) {
|
|
42
|
+
output.json({ server: config.serverUrl, vault: unlocked ? "unlocked" : "locked", connected: false, error: `HTTP ${res.status}` });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log(`Server: ${config.serverUrl}`);
|
|
46
|
+
console.log(`Vault: ${unlocked ? "Unlocked" : "Locked"}`);
|
|
47
|
+
output.error(`Server returned ${res.status}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (options.json) {
|
|
53
|
+
output.json({ server: config.serverUrl, vault: unlocked ? "unlocked" : "locked", connected: false, error: err instanceof Error ? err.message : "unknown error" });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.log(`Server: ${config.serverUrl}`);
|
|
57
|
+
console.log(`Vault: ${unlocked ? "Unlocked" : "Locked"}`);
|
|
58
|
+
output.error(`Connection failed: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `passwd-sso totp <id>` — Generate TOTP code for an entry.
|
|
3
|
+
*/
|
|
4
|
+
import { apiRequest } from "../lib/api-client.js";
|
|
5
|
+
import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
|
|
6
|
+
import { decryptData } from "../lib/crypto.js";
|
|
7
|
+
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
8
|
+
import { generateTOTPCode } from "../lib/totp.js";
|
|
9
|
+
import { copyToClipboard } from "../lib/clipboard.js";
|
|
10
|
+
import * as output from "../lib/output.js";
|
|
11
|
+
export async function totpCommand(id, options) {
|
|
12
|
+
const key = getEncryptionKey();
|
|
13
|
+
if (!key) {
|
|
14
|
+
output.error("Vault is locked. Run `unlock` first.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const userId = getUserId();
|
|
18
|
+
const res = await apiRequest(`/api/passwords/${id}`);
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
output.error(`Entry not found: ${res.status}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const aad = res.data.aadVersion >= 1 && userId
|
|
25
|
+
? buildPersonalEntryAAD(userId, res.data.id)
|
|
26
|
+
: undefined;
|
|
27
|
+
const plaintext = await decryptData(res.data.encryptedBlob, key, aad);
|
|
28
|
+
const blob = JSON.parse(plaintext);
|
|
29
|
+
if (!blob.totp?.secret) {
|
|
30
|
+
output.error("No TOTP configured for this entry.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const code = generateTOTPCode({
|
|
34
|
+
secret: blob.totp.secret,
|
|
35
|
+
algorithm: blob.totp.algorithm,
|
|
36
|
+
digits: blob.totp.digits,
|
|
37
|
+
period: blob.totp.period,
|
|
38
|
+
});
|
|
39
|
+
const period = blob.totp.period ?? 30;
|
|
40
|
+
const remaining = period - (Math.floor(Date.now() / 1000) % period);
|
|
41
|
+
if (options.json) {
|
|
42
|
+
output.json({ code, remaining, period });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (options.copy) {
|
|
46
|
+
await copyToClipboard(code);
|
|
47
|
+
output.success(`TOTP: ${code} (expires in ${remaining}s) — copied to clipboard`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(`${code} (${remaining}s remaining)`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
output.error("Failed to generate TOTP code.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=totp.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `passwd-sso unlock` — Unlock the vault with passphrase.
|
|
3
|
+
*
|
|
4
|
+
* Derives the encryption key and stores it in process memory.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readPassphrase(prompt: string, opts?: {
|
|
7
|
+
useStderr?: boolean;
|
|
8
|
+
}): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Core unlock logic — derives encryption key from passphrase and stores it.
|
|
11
|
+
* Returns true on success, false on failure.
|
|
12
|
+
*/
|
|
13
|
+
export declare function unlockWithPassphrase(passphrase: string): Promise<boolean>;
|
|
14
|
+
export declare function unlockCommand(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Auto-unlock using PSSO_PASSPHRASE env var if vault is locked.
|
|
17
|
+
* Returns true if vault is unlocked (already or just now), false otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export declare function autoUnlockIfNeeded(): Promise<boolean>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `passwd-sso unlock` — Unlock the vault with passphrase.
|
|
3
|
+
*
|
|
4
|
+
* Derives the encryption key and stores it in process memory.
|
|
5
|
+
*/
|
|
6
|
+
import { apiRequest } from "../lib/api-client.js";
|
|
7
|
+
import { hexDecode, deriveWrappingKey, unwrapSecretKey, deriveEncryptionKey, verifyKey, } from "../lib/crypto.js";
|
|
8
|
+
import { setEncryptionKey, setSecretKeyBytes, isUnlocked } from "../lib/vault-state.js";
|
|
9
|
+
import * as output from "../lib/output.js";
|
|
10
|
+
export async function readPassphrase(prompt, opts) {
|
|
11
|
+
// When used with eval $(...), stdout is captured by the shell.
|
|
12
|
+
// Write prompt to stderr so it's visible but not evaluated.
|
|
13
|
+
const out = opts?.useStderr ? process.stderr : process.stdout;
|
|
14
|
+
out.write(prompt);
|
|
15
|
+
if (process.stdin.isTTY) {
|
|
16
|
+
process.stdin.setRawMode?.(true);
|
|
17
|
+
}
|
|
18
|
+
process.stdin.resume();
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
let passphrase = "";
|
|
21
|
+
const onData = (key) => {
|
|
22
|
+
const char = key.toString("utf-8");
|
|
23
|
+
if (char === "\n" || char === "\r" || char === "\u0003") {
|
|
24
|
+
process.stdin.removeListener("data", onData);
|
|
25
|
+
process.stdin.removeListener("end", onEnd);
|
|
26
|
+
if (process.stdin.isTTY) {
|
|
27
|
+
process.stdin.setRawMode?.(false);
|
|
28
|
+
}
|
|
29
|
+
process.stdin.pause();
|
|
30
|
+
console.log(); // newline after hidden input
|
|
31
|
+
if (char === "\u0003") {
|
|
32
|
+
process.exit(130);
|
|
33
|
+
}
|
|
34
|
+
resolve(passphrase);
|
|
35
|
+
}
|
|
36
|
+
else if (char === "\u007F" || char === "\b") {
|
|
37
|
+
passphrase = passphrase.slice(0, -1);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
passphrase += char;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const onEnd = () => {
|
|
44
|
+
process.stdin.removeListener("data", onData);
|
|
45
|
+
process.stdin.removeListener("end", onEnd);
|
|
46
|
+
if (process.stdin.isTTY) {
|
|
47
|
+
process.stdin.setRawMode?.(false);
|
|
48
|
+
}
|
|
49
|
+
process.stdin.pause();
|
|
50
|
+
console.log();
|
|
51
|
+
resolve(passphrase);
|
|
52
|
+
};
|
|
53
|
+
process.stdin.on("data", onData);
|
|
54
|
+
process.stdin.on("end", onEnd);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Core unlock logic — derives encryption key from passphrase and stores it.
|
|
59
|
+
* Returns true on success, false on failure.
|
|
60
|
+
*/
|
|
61
|
+
export async function unlockWithPassphrase(passphrase) {
|
|
62
|
+
const res = await apiRequest("/api/vault/unlock/data");
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
output.error(`Failed to fetch vault data: ${res.status}`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const data = res.data;
|
|
68
|
+
if (!data.accountSalt || !data.encryptedSecretKey) {
|
|
69
|
+
output.error("Vault is not set up. Please set up your vault in the web UI first.");
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const accountSalt = hexDecode(data.accountSalt);
|
|
74
|
+
const wrappingKey = await deriveWrappingKey(passphrase, accountSalt);
|
|
75
|
+
const encryptedSecret = {
|
|
76
|
+
ciphertext: data.encryptedSecretKey,
|
|
77
|
+
iv: data.secretKeyIv,
|
|
78
|
+
authTag: data.secretKeyAuthTag,
|
|
79
|
+
};
|
|
80
|
+
const secretKey = await unwrapSecretKey(encryptedSecret, wrappingKey);
|
|
81
|
+
const encryptionKey = await deriveEncryptionKey(secretKey);
|
|
82
|
+
if (data.verificationArtifact) {
|
|
83
|
+
const valid = await verifyKey(encryptionKey, data.verificationArtifact);
|
|
84
|
+
if (!valid) {
|
|
85
|
+
output.error("Incorrect passphrase.");
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
setEncryptionKey(encryptionKey, data.userId);
|
|
90
|
+
setSecretKeyBytes(secretKey);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
output.error("Failed to unlock vault. Check your passphrase.");
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export async function unlockCommand() {
|
|
99
|
+
if (isUnlocked()) {
|
|
100
|
+
output.info("Vault is already unlocked.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const passphrase = await readPassphrase("Master passphrase: ");
|
|
104
|
+
if (!passphrase) {
|
|
105
|
+
output.error("Passphrase is required.");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (await unlockWithPassphrase(passphrase)) {
|
|
109
|
+
output.success("Vault unlocked.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Auto-unlock using PSSO_PASSPHRASE env var if vault is locked.
|
|
114
|
+
* Returns true if vault is unlocked (already or just now), false otherwise.
|
|
115
|
+
*/
|
|
116
|
+
export async function autoUnlockIfNeeded() {
|
|
117
|
+
if (isUnlocked())
|
|
118
|
+
return true;
|
|
119
|
+
const passphrase = process.env.PSSO_PASSPHRASE;
|
|
120
|
+
if (!passphrase) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return unlockWithPassphrase(passphrase);
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=unlock.js.map
|
package/dist/index.d.ts
ADDED