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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for the CLI tool.
|
|
3
|
+
*
|
|
4
|
+
* Config file: $XDG_CONFIG_HOME/passwd-sso/config.json
|
|
5
|
+
* Credentials: OS keychain (via keytar) or $XDG_DATA_HOME/passwd-sso/credentials
|
|
6
|
+
*
|
|
7
|
+
* Legacy ~/.passwd-sso/ is auto-migrated on first access.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, lstatSync, openSync, writeSync, closeSync, constants as fsConstants } from "node:fs";
|
|
10
|
+
import { getConfigDir, getDataDir, getConfigFilePath, getCredentialsFilePath, } from "./paths.js";
|
|
11
|
+
import { migrateIfNeeded } from "./migrate.js";
|
|
12
|
+
const KEYCHAIN_SERVICE = "passwd-sso-cli";
|
|
13
|
+
const KEYCHAIN_ACCOUNT = "bearer-token";
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
serverUrl: "",
|
|
16
|
+
locale: "en",
|
|
17
|
+
};
|
|
18
|
+
function ensureConfigDir() {
|
|
19
|
+
const dir = getConfigDir();
|
|
20
|
+
if (!existsSync(dir)) {
|
|
21
|
+
mkdirSync(dir, { mode: 0o700, recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function ensureDataDir() {
|
|
25
|
+
const dir = getDataDir();
|
|
26
|
+
if (!existsSync(dir)) {
|
|
27
|
+
mkdirSync(dir, { mode: 0o700, recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function loadConfig() {
|
|
31
|
+
migrateIfNeeded();
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(getConfigFilePath(), "utf-8");
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { ...DEFAULT_CONFIG };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function saveConfig(config) {
|
|
42
|
+
ensureConfigDir();
|
|
43
|
+
writeFileSync(getConfigFilePath(), JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
44
|
+
}
|
|
45
|
+
// ─── Credential Storage ───────────────────────────────────────
|
|
46
|
+
async function tryKeytar() {
|
|
47
|
+
if (process.env.PSSO_NO_KEYCHAIN === "1")
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const mod = await import("keytar");
|
|
51
|
+
// Dynamic import may wrap the CJS module in { default: ... }
|
|
52
|
+
return (mod.default ?? mod);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function saveToken(token) {
|
|
59
|
+
const kt = await tryKeytar();
|
|
60
|
+
if (kt) {
|
|
61
|
+
await kt.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, token);
|
|
62
|
+
return "keychain";
|
|
63
|
+
}
|
|
64
|
+
// File fallback — use O_NOFOLLOW to prevent symlink attacks (TOCTOU-safe)
|
|
65
|
+
ensureDataDir();
|
|
66
|
+
const dataDir = getDataDir();
|
|
67
|
+
const stat = lstatSync(dataDir);
|
|
68
|
+
if (stat.isSymbolicLink()) {
|
|
69
|
+
throw new Error("Data directory is a symlink — refusing to write credentials.");
|
|
70
|
+
}
|
|
71
|
+
const credPath = getCredentialsFilePath();
|
|
72
|
+
const fd = openSync(credPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | (fsConstants.O_NOFOLLOW ?? 0), 0o600);
|
|
73
|
+
try {
|
|
74
|
+
writeSync(fd, token);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
return "file";
|
|
80
|
+
}
|
|
81
|
+
export async function loadToken() {
|
|
82
|
+
migrateIfNeeded();
|
|
83
|
+
const kt = await tryKeytar();
|
|
84
|
+
if (kt) {
|
|
85
|
+
const token = await kt.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
86
|
+
if (token)
|
|
87
|
+
return token;
|
|
88
|
+
}
|
|
89
|
+
// File fallback
|
|
90
|
+
try {
|
|
91
|
+
return readFileSync(getCredentialsFilePath(), "utf-8").trim();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function deleteToken() {
|
|
98
|
+
const kt = await tryKeytar();
|
|
99
|
+
if (kt) {
|
|
100
|
+
await kt.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const { unlinkSync } = await import("node:fs");
|
|
104
|
+
unlinkSync(getCredentialsFilePath());
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// file may not exist
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AAD (Additional Authenticated Data) builders for AES-256-GCM encryption.
|
|
3
|
+
* Ported from src/lib/crypto-aad.ts for CLI compatibility.
|
|
4
|
+
*/
|
|
5
|
+
const AAD_VERSION = 1;
|
|
6
|
+
const SCOPE_PERSONAL = "PV";
|
|
7
|
+
function buildAADBytes(scope, expectedFieldCount, fields) {
|
|
8
|
+
if (scope.length !== 2) {
|
|
9
|
+
throw new Error(`AAD scope must be exactly 2 ASCII chars, got "${scope}"`);
|
|
10
|
+
}
|
|
11
|
+
if (fields.length !== expectedFieldCount) {
|
|
12
|
+
throw new Error(`AAD scope "${scope}" expects ${expectedFieldCount} fields, got ${fields.length}`);
|
|
13
|
+
}
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
const encodedFields = fields.map((f) => encoder.encode(f));
|
|
16
|
+
const headerSize = 4;
|
|
17
|
+
const fieldsSize = encodedFields.reduce((sum, ef) => sum + 2 + ef.length, 0);
|
|
18
|
+
const totalSize = headerSize + fieldsSize;
|
|
19
|
+
const buf = new ArrayBuffer(totalSize);
|
|
20
|
+
const view = new DataView(buf);
|
|
21
|
+
const bytes = new Uint8Array(buf);
|
|
22
|
+
let offset = 0;
|
|
23
|
+
bytes[offset] = scope.charCodeAt(0);
|
|
24
|
+
bytes[offset + 1] = scope.charCodeAt(1);
|
|
25
|
+
offset += 2;
|
|
26
|
+
view.setUint8(offset, AAD_VERSION);
|
|
27
|
+
offset += 1;
|
|
28
|
+
view.setUint8(offset, fields.length);
|
|
29
|
+
offset += 1;
|
|
30
|
+
for (const encoded of encodedFields) {
|
|
31
|
+
if (encoded.length > 0xffff) {
|
|
32
|
+
throw new Error(`AAD field too long: ${encoded.length} bytes (max 65535)`);
|
|
33
|
+
}
|
|
34
|
+
view.setUint16(offset, encoded.length, false);
|
|
35
|
+
offset += 2;
|
|
36
|
+
bytes.set(encoded, offset);
|
|
37
|
+
offset += encoded.length;
|
|
38
|
+
}
|
|
39
|
+
return bytes;
|
|
40
|
+
}
|
|
41
|
+
export function buildPersonalEntryAAD(userId, entryId) {
|
|
42
|
+
return buildAADBytes(SCOPE_PERSONAL, 2, [userId, entryId]);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=crypto-aad.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto module for the CLI tool using Node.js crypto.subtle (Web Crypto API compatible).
|
|
3
|
+
*
|
|
4
|
+
* Ported from src/lib/crypto-client.ts — identical key derivation and encryption
|
|
5
|
+
* to ensure interoperability with the Web UI.
|
|
6
|
+
*/
|
|
7
|
+
export interface EncryptedData {
|
|
8
|
+
ciphertext: string;
|
|
9
|
+
iv: string;
|
|
10
|
+
authTag: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function hexEncode(buf: ArrayBuffer | Uint8Array): string;
|
|
13
|
+
export declare function hexDecode(hex: string): Uint8Array;
|
|
14
|
+
export declare function deriveWrappingKey(passphrase: string, accountSalt: Uint8Array): Promise<CryptoKey>;
|
|
15
|
+
export declare function deriveEncryptionKey(secretKey: Uint8Array): Promise<CryptoKey>;
|
|
16
|
+
export declare function deriveAuthKey(secretKey: Uint8Array): Promise<CryptoKey>;
|
|
17
|
+
export declare function unwrapSecretKey(encrypted: EncryptedData, wrappingKey: CryptoKey): Promise<Uint8Array>;
|
|
18
|
+
export declare function computeAuthHash(authKey: CryptoKey): Promise<string>;
|
|
19
|
+
export declare function verifyKey(encryptionKey: CryptoKey, artifact: EncryptedData): Promise<boolean>;
|
|
20
|
+
export declare function encryptData(plaintext: string, key: CryptoKey, aad?: Uint8Array): Promise<EncryptedData>;
|
|
21
|
+
export declare function decryptData(encrypted: EncryptedData, key: CryptoKey, aad?: Uint8Array): Promise<string>;
|
|
22
|
+
export declare function deriveVerifierSalt(accountSalt: Uint8Array): Promise<Uint8Array>;
|
|
23
|
+
export declare function computePassphraseVerifier(passphrase: string, accountSalt: Uint8Array): Promise<string>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto module for the CLI tool using Node.js crypto.subtle (Web Crypto API compatible).
|
|
3
|
+
*
|
|
4
|
+
* Ported from src/lib/crypto-client.ts — identical key derivation and encryption
|
|
5
|
+
* to ensure interoperability with the Web UI.
|
|
6
|
+
*/
|
|
7
|
+
const PBKDF2_ITERATIONS = 600_000;
|
|
8
|
+
const AES_KEY_LENGTH = 256;
|
|
9
|
+
const IV_LENGTH = 12;
|
|
10
|
+
const HKDF_ENC_INFO = "passwd-sso-enc-v1";
|
|
11
|
+
const HKDF_AUTH_INFO = "passwd-sso-auth-v1";
|
|
12
|
+
const VERIFICATION_PLAINTEXT = "passwd-sso-vault-verification-v1";
|
|
13
|
+
const VERIFIER_DOMAIN_PREFIX = "verifier";
|
|
14
|
+
const VERIFIER_PBKDF2_HASH = "SHA-256";
|
|
15
|
+
const VERIFIER_PBKDF2_ITERATIONS = 600_000;
|
|
16
|
+
const VERIFIER_PBKDF2_BITS = 256;
|
|
17
|
+
// ─── Utility ──────────────────────────────────────────────────
|
|
18
|
+
export function hexEncode(buf) {
|
|
19
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
20
|
+
return Array.from(bytes)
|
|
21
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
22
|
+
.join("");
|
|
23
|
+
}
|
|
24
|
+
export function hexDecode(hex) {
|
|
25
|
+
if (hex.length % 2 !== 0) {
|
|
26
|
+
throw new Error("Invalid hex string: odd length");
|
|
27
|
+
}
|
|
28
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
29
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
30
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
31
|
+
}
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
function toArrayBuffer(arr) {
|
|
35
|
+
return arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
|
|
36
|
+
}
|
|
37
|
+
function textEncode(text) {
|
|
38
|
+
return toArrayBuffer(new TextEncoder().encode(text));
|
|
39
|
+
}
|
|
40
|
+
function textDecode(buf) {
|
|
41
|
+
return new TextDecoder().decode(buf);
|
|
42
|
+
}
|
|
43
|
+
// ─── Key Derivation ────────────────────────────────────────────
|
|
44
|
+
export async function deriveWrappingKey(passphrase, accountSalt) {
|
|
45
|
+
const keyMaterial = await crypto.subtle.importKey("raw", textEncode(passphrase), "PBKDF2", false, ["deriveKey"]);
|
|
46
|
+
return crypto.subtle.deriveKey({
|
|
47
|
+
name: "PBKDF2",
|
|
48
|
+
salt: toArrayBuffer(accountSalt),
|
|
49
|
+
iterations: PBKDF2_ITERATIONS,
|
|
50
|
+
hash: "SHA-256",
|
|
51
|
+
}, keyMaterial, { name: "AES-GCM", length: AES_KEY_LENGTH }, false, ["encrypt", "decrypt"]);
|
|
52
|
+
}
|
|
53
|
+
export async function deriveEncryptionKey(secretKey) {
|
|
54
|
+
const hkdfKey = await crypto.subtle.importKey("raw", toArrayBuffer(secretKey), "HKDF", false, ["deriveKey"]);
|
|
55
|
+
return crypto.subtle.deriveKey({
|
|
56
|
+
name: "HKDF",
|
|
57
|
+
hash: "SHA-256",
|
|
58
|
+
salt: new ArrayBuffer(32),
|
|
59
|
+
info: textEncode(HKDF_ENC_INFO),
|
|
60
|
+
}, hkdfKey, { name: "AES-GCM", length: AES_KEY_LENGTH }, false, ["encrypt", "decrypt"]);
|
|
61
|
+
}
|
|
62
|
+
export async function deriveAuthKey(secretKey) {
|
|
63
|
+
const hkdfKey = await crypto.subtle.importKey("raw", toArrayBuffer(secretKey), "HKDF", false, ["deriveKey"]);
|
|
64
|
+
return crypto.subtle.deriveKey({
|
|
65
|
+
name: "HKDF",
|
|
66
|
+
hash: "SHA-256",
|
|
67
|
+
salt: new ArrayBuffer(32),
|
|
68
|
+
info: textEncode(HKDF_AUTH_INFO),
|
|
69
|
+
}, hkdfKey, { name: "HMAC", hash: "SHA-256", length: AES_KEY_LENGTH }, true, ["sign"]);
|
|
70
|
+
}
|
|
71
|
+
// ─── Secret Key Management ─────────────────────────────────────
|
|
72
|
+
export async function unwrapSecretKey(encrypted, wrappingKey) {
|
|
73
|
+
const ciphertext = hexDecode(encrypted.ciphertext);
|
|
74
|
+
const iv = hexDecode(encrypted.iv);
|
|
75
|
+
const authTag = hexDecode(encrypted.authTag);
|
|
76
|
+
const combined = new Uint8Array(ciphertext.length + authTag.length);
|
|
77
|
+
combined.set(ciphertext);
|
|
78
|
+
combined.set(authTag, ciphertext.length);
|
|
79
|
+
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: toArrayBuffer(iv) }, wrappingKey, toArrayBuffer(combined));
|
|
80
|
+
return new Uint8Array(decrypted);
|
|
81
|
+
}
|
|
82
|
+
// ─── Auth Hash ────────────────────────────────────────────────
|
|
83
|
+
export async function computeAuthHash(authKey) {
|
|
84
|
+
const rawKey = await crypto.subtle.exportKey("raw", authKey);
|
|
85
|
+
const hash = await crypto.subtle.digest("SHA-256", rawKey);
|
|
86
|
+
return hexEncode(hash);
|
|
87
|
+
}
|
|
88
|
+
// ─── Verification ──────────────────────────────────────────────
|
|
89
|
+
export async function verifyKey(encryptionKey, artifact) {
|
|
90
|
+
try {
|
|
91
|
+
const plaintext = await decryptData(artifact, encryptionKey);
|
|
92
|
+
return plaintext === VERIFICATION_PLAINTEXT;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// ─── E2E Encryption ────────────────────────────────────────────
|
|
99
|
+
export async function encryptData(plaintext, key, aad) {
|
|
100
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
101
|
+
const params = { name: "AES-GCM", iv: toArrayBuffer(iv) };
|
|
102
|
+
if (aad)
|
|
103
|
+
params.additionalData = toArrayBuffer(aad);
|
|
104
|
+
const encrypted = await crypto.subtle.encrypt(params, key, textEncode(plaintext));
|
|
105
|
+
const encryptedBytes = new Uint8Array(encrypted);
|
|
106
|
+
const ciphertext = encryptedBytes.slice(0, encryptedBytes.length - 16);
|
|
107
|
+
const authTag = encryptedBytes.slice(encryptedBytes.length - 16);
|
|
108
|
+
return {
|
|
109
|
+
ciphertext: hexEncode(ciphertext),
|
|
110
|
+
iv: hexEncode(iv),
|
|
111
|
+
authTag: hexEncode(authTag),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export async function decryptData(encrypted, key, aad) {
|
|
115
|
+
const ciphertext = hexDecode(encrypted.ciphertext);
|
|
116
|
+
const iv = hexDecode(encrypted.iv);
|
|
117
|
+
const authTag = hexDecode(encrypted.authTag);
|
|
118
|
+
const combined = new Uint8Array(ciphertext.length + authTag.length);
|
|
119
|
+
combined.set(ciphertext);
|
|
120
|
+
combined.set(authTag, ciphertext.length);
|
|
121
|
+
const params = { name: "AES-GCM", iv: toArrayBuffer(iv) };
|
|
122
|
+
if (aad)
|
|
123
|
+
params.additionalData = toArrayBuffer(aad);
|
|
124
|
+
const decrypted = await crypto.subtle.decrypt(params, key, toArrayBuffer(combined));
|
|
125
|
+
return textDecode(decrypted);
|
|
126
|
+
}
|
|
127
|
+
// ─── Passphrase Verifier ──────────────────────────────────────
|
|
128
|
+
export async function deriveVerifierSalt(accountSalt) {
|
|
129
|
+
const prefix = new TextEncoder().encode(VERIFIER_DOMAIN_PREFIX);
|
|
130
|
+
const combined = new Uint8Array(prefix.length + accountSalt.length);
|
|
131
|
+
combined.set(prefix);
|
|
132
|
+
combined.set(accountSalt, prefix.length);
|
|
133
|
+
const hash = await crypto.subtle.digest("SHA-256", toArrayBuffer(combined));
|
|
134
|
+
return new Uint8Array(hash);
|
|
135
|
+
}
|
|
136
|
+
export async function computePassphraseVerifier(passphrase, accountSalt) {
|
|
137
|
+
const verifierSalt = await deriveVerifierSalt(accountSalt);
|
|
138
|
+
const keyMaterial = await crypto.subtle.importKey("raw", textEncode(passphrase), "PBKDF2", false, ["deriveBits"]);
|
|
139
|
+
const verifierKeyBits = await crypto.subtle.deriveBits({
|
|
140
|
+
name: "PBKDF2",
|
|
141
|
+
salt: toArrayBuffer(verifierSalt),
|
|
142
|
+
iterations: VERIFIER_PBKDF2_ITERATIONS,
|
|
143
|
+
hash: VERIFIER_PBKDF2_HASH,
|
|
144
|
+
}, keyMaterial, VERIFIER_PBKDF2_BITS);
|
|
145
|
+
const verifierHash = await crypto.subtle.digest("SHA-256", verifierKeyBits);
|
|
146
|
+
return hexEncode(verifierHash);
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-migration from legacy ~/.passwd-sso/ to XDG directories.
|
|
3
|
+
*
|
|
4
|
+
* Called once on first access. Idempotent — safe to call multiple times.
|
|
5
|
+
*/
|
|
6
|
+
export declare function migrateIfNeeded(): void;
|
|
7
|
+
/** Reset migration state (for testing only). */
|
|
8
|
+
export declare function _resetMigrationState(): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-migration from legacy ~/.passwd-sso/ to XDG directories.
|
|
3
|
+
*
|
|
4
|
+
* Called once on first access. Idempotent — safe to call multiple times.
|
|
5
|
+
*/
|
|
6
|
+
import { copyFileSync, chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, unlinkSync, } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { getLegacyDir, getConfigDir, getDataDir } from "./paths.js";
|
|
9
|
+
let migrationDone = false;
|
|
10
|
+
/**
|
|
11
|
+
* Move a single file from `src` to `dest`, creating `destDir` if needed.
|
|
12
|
+
* Handles cross-device moves (EXDEV) by falling back to copy + delete.
|
|
13
|
+
*/
|
|
14
|
+
function moveFile(src, dest, destDir) {
|
|
15
|
+
try {
|
|
16
|
+
mkdirSync(destDir, { mode: 0o700, recursive: true });
|
|
17
|
+
renameSync(src, dest);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err.code === "EXDEV") {
|
|
22
|
+
try {
|
|
23
|
+
copyFileSync(src, dest);
|
|
24
|
+
chmodSync(dest, 0o600);
|
|
25
|
+
unlinkSync(src);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function migrateIfNeeded() {
|
|
36
|
+
if (migrationDone)
|
|
37
|
+
return;
|
|
38
|
+
migrationDone = true;
|
|
39
|
+
const legacyDir = getLegacyDir();
|
|
40
|
+
if (!existsSync(legacyDir))
|
|
41
|
+
return;
|
|
42
|
+
// Safety: refuse to follow symlinks
|
|
43
|
+
const stat = lstatSync(legacyDir);
|
|
44
|
+
if (stat.isSymbolicLink())
|
|
45
|
+
return;
|
|
46
|
+
const legacyConfig = join(legacyDir, "config.json");
|
|
47
|
+
const legacyCredentials = join(legacyDir, "credentials");
|
|
48
|
+
let migratedAny = false;
|
|
49
|
+
// Migrate config.json → XDG_CONFIG_HOME/passwd-sso/config.json
|
|
50
|
+
if (existsSync(legacyConfig)) {
|
|
51
|
+
const configDir = getConfigDir();
|
|
52
|
+
const target = join(configDir, "config.json");
|
|
53
|
+
if (!existsSync(target)) {
|
|
54
|
+
if (moveFile(legacyConfig, target, configDir)) {
|
|
55
|
+
migratedAny = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Migrate credentials → XDG_DATA_HOME/passwd-sso/credentials
|
|
60
|
+
if (existsSync(legacyCredentials)) {
|
|
61
|
+
const dataDir = getDataDir();
|
|
62
|
+
const target = join(dataDir, "credentials");
|
|
63
|
+
if (!existsSync(target)) {
|
|
64
|
+
if (moveFile(legacyCredentials, target, dataDir)) {
|
|
65
|
+
migratedAny = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Remove legacy directory if empty
|
|
70
|
+
if (migratedAny) {
|
|
71
|
+
try {
|
|
72
|
+
const remaining = readdirSync(legacyDir);
|
|
73
|
+
if (remaining.length === 0) {
|
|
74
|
+
rmSync(legacyDir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Best effort — leave it if removal fails
|
|
79
|
+
}
|
|
80
|
+
console.error(`[passwd-sso] Migrated config to XDG directories. Config: ${getConfigDir()}, Data: ${getDataDir()}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Reset migration state (for testing only). */
|
|
84
|
+
export function _resetMigrationState() {
|
|
85
|
+
migrationDone = false;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenSSH private key format parser.
|
|
3
|
+
*
|
|
4
|
+
* Handles `-----BEGIN OPENSSH PRIVATE KEY-----` format that
|
|
5
|
+
* Node.js/OpenSSL `createPrivateKey()` cannot always parse
|
|
6
|
+
* (notably encrypted keys using bcrypt-pbkdf).
|
|
7
|
+
*
|
|
8
|
+
* Supports: Ed25519, RSA, ECDSA (encrypted and unencrypted).
|
|
9
|
+
*
|
|
10
|
+
* Reference: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
|
|
11
|
+
*/
|
|
12
|
+
import type { KeyObject } from "node:crypto";
|
|
13
|
+
/**
|
|
14
|
+
* Parse an OpenSSH private key PEM into a Node.js KeyObject.
|
|
15
|
+
* Falls back to this when `createPrivateKey()` can't handle the format.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseOpenSshPrivateKey(pem: string, passphrase?: string): Promise<KeyObject>;
|