[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
448
domain/credentials.ts
Normal file
448
domain/credentials.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
import type { SyncPayload } from "./sync";
|
||||
|
||||
const CREDENTIAL_ENCRYPTION_PREFIX = "enc:v1:";
|
||||
|
||||
/**
|
||||
* Base64 pattern: only allows A-Z, a-z, 0-9, +, / and trailing = padding.
|
||||
*/
|
||||
const BASE64_RE = /^[A-Za-z0-9+/]+=*$/;
|
||||
|
||||
/**
|
||||
* Chromium/Electron safeStorage ciphertext carries known platform headers:
|
||||
* - macOS/Linux: plaintext bytes start with "v10" or "v11"
|
||||
* - Windows (legacy DPAPI blob): leading bytes are 0x01 0x00 0x00 0x00
|
||||
*
|
||||
* Detect headers on *decoded* bytes. A four-byte DPAPI version alone is not
|
||||
* enough — real blobs continue with provider GUID
|
||||
* {df9d8cd0-1501-11d1-8c7a-00c04fc297eb} (base64 `AQAAANCMnd8...`).
|
||||
*
|
||||
* We require a known header AND a complete-enough decoded blob. v10/v11 CBC
|
||||
* blobs are at least header(3) + one AES block(16) = 19 bytes. Header-only
|
||||
* base64 such as `enc:v1:djEw` must not be treated as ciphertext.
|
||||
*
|
||||
* Keep in sync with electron/bridges/credentialBridge.cjs.
|
||||
*
|
||||
* References:
|
||||
* - components/os_crypt/sync/os_crypt_mac.mm (kObfuscationPrefixV10 = "v10")
|
||||
* - components/os_crypt/sync/os_crypt_linux.cc (kObfuscationPrefixV10/V11)
|
||||
* - components/os_crypt/sync/os_crypt_win.cc (DPAPI legacy path)
|
||||
*/
|
||||
const V10_HEADER = [0x76, 0x31, 0x30] as const; // "v10"
|
||||
const V11_HEADER = [0x76, 0x31, 0x31] as const; // "v11"
|
||||
// Version (4) + provider GUID {df9d8cd0-1501-11d1-8c7a-00c04fc297eb} (16).
|
||||
const DPAPI_BLOB_PREFIX = [
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0xd0, 0x8c, 0x9d, 0xdf, 0x01, 0x15, 0xd1, 0x11,
|
||||
0x8c, 0x7a, 0x00, 0xc0, 0x4f, 0xc2, 0x97, 0xeb,
|
||||
] as const;
|
||||
|
||||
/** Minimum decoded sizes for complete Chromium OSCrypt blobs. */
|
||||
const MIN_V10_V11_CIPHERTEXT_BYTES = 19; // CBC: header(3) + one AES block(16)
|
||||
const MIN_V10_V11_GCM_CIPHERTEXT_BYTES = 31; // header(3) + nonce(12) + tag(16)
|
||||
const MIN_DPAPI_CIPHERTEXT_BYTES = DPAPI_BLOB_PREFIX.length + 1;
|
||||
|
||||
/**
|
||||
* Renderer-safe base64 decode. Avoids Node `Buffer` which is unavailable
|
||||
* in Electron windows with `nodeIntegration: false`.
|
||||
*/
|
||||
const decodeBase64Bytes = (payload: string): Uint8Array | null => {
|
||||
try {
|
||||
if (typeof atob === "function") {
|
||||
const binary = atob(payload);
|
||||
const out = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
out[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
// Node / test environments without atob.
|
||||
if (typeof Buffer !== "undefined") {
|
||||
try {
|
||||
return new Uint8Array(Buffer.from(payload, "base64"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const startsWithBytes = (
|
||||
decoded: Uint8Array,
|
||||
prefix: readonly number[],
|
||||
): boolean => {
|
||||
if (decoded.byteLength < prefix.length) return false;
|
||||
return prefix.every((byte, index) => decoded[index] === byte);
|
||||
};
|
||||
|
||||
/** CBC is header(3) + 16-byte blocks; GCM is at least header+nonce+tag (31). */
|
||||
const isValidV10V11CiphertextLength = (byteLength: number): boolean => {
|
||||
if (byteLength >= MIN_V10_V11_GCM_CIPHERTEXT_BYTES) return true;
|
||||
return byteLength >= MIN_V10_V11_CIPHERTEXT_BYTES
|
||||
&& (byteLength - 3) % 16 === 0;
|
||||
};
|
||||
|
||||
export const isEncryptedCredentialPlaceholder = (
|
||||
value: string | undefined | null,
|
||||
): value is string => {
|
||||
if (typeof value !== "string" || !value.startsWith(CREDENTIAL_ENCRYPTION_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
const payload = value.slice(CREDENTIAL_ENCRYPTION_PREFIX.length);
|
||||
if (!payload || !BASE64_RE.test(payload)) return false;
|
||||
const decoded = decodeBase64Bytes(payload);
|
||||
if (!decoded) return false;
|
||||
if (startsWithBytes(decoded, V10_HEADER) || startsWithBytes(decoded, V11_HEADER)) {
|
||||
return isValidV10V11CiphertextLength(decoded.byteLength);
|
||||
}
|
||||
if (startsWithBytes(decoded, DPAPI_BLOB_PREFIX)) {
|
||||
return decoded.byteLength >= MIN_DPAPI_CIPHERTEXT_BYTES;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip enc:v1: placeholders from a single credential value.
|
||||
* Used at the terminal connection boundary to avoid sending encrypted
|
||||
* placeholders as actual passwords to SSH/Telnet servers.
|
||||
*/
|
||||
export const sanitizeCredentialValue = (
|
||||
value: string | undefined,
|
||||
): string | undefined => {
|
||||
if (isEncryptedCredentialPlaceholder(value)) return undefined;
|
||||
return value;
|
||||
};
|
||||
|
||||
export const isVaultStoredKeySource = (
|
||||
source: string | undefined,
|
||||
): source is "imported" | "generated" =>
|
||||
source === "imported" || source === "generated";
|
||||
|
||||
/**
|
||||
* Imported/generated keys store private material in the vault. Empty or
|
||||
* still-encrypted privateKey means hydration has not finished (or failed).
|
||||
*/
|
||||
export const needsVaultStoredKeyHydration = (
|
||||
key?: { source?: string; privateKey?: string } | null,
|
||||
): boolean => {
|
||||
if (!key || !isVaultStoredKeySource(key.source)) return false;
|
||||
return !key.privateKey || isEncryptedCredentialPlaceholder(key.privateKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* Scan a sync payload for any fields that still carry device-bound
|
||||
* enc:v1: ciphertext. Returns the dotted paths of offending fields.
|
||||
* Used as a pre-upload guard to prevent pushing un-decryptable data.
|
||||
*/
|
||||
export const findSyncPayloadEncryptedCredentialPaths = (
|
||||
payload: SyncPayload,
|
||||
): string[] => {
|
||||
const issues: string[] = [];
|
||||
|
||||
payload.hosts.forEach((host, index) => {
|
||||
if (isEncryptedCredentialPlaceholder(host.password)) {
|
||||
issues.push(`hosts[${index}].password`);
|
||||
}
|
||||
if (isEncryptedCredentialPlaceholder(host.telnetPassword)) {
|
||||
issues.push(`hosts[${index}].telnetPassword`);
|
||||
}
|
||||
if (isEncryptedCredentialPlaceholder(host.proxyConfig?.password)) {
|
||||
issues.push(`hosts[${index}].proxyConfig.password`);
|
||||
}
|
||||
});
|
||||
|
||||
payload.keys.forEach((key, index) => {
|
||||
if (isEncryptedCredentialPlaceholder(key.privateKey)) {
|
||||
issues.push(`keys[${index}].privateKey`);
|
||||
}
|
||||
if (isEncryptedCredentialPlaceholder(key.passphrase)) {
|
||||
issues.push(`keys[${index}].passphrase`);
|
||||
}
|
||||
});
|
||||
|
||||
payload.identities?.forEach((identity, index) => {
|
||||
if (isEncryptedCredentialPlaceholder(identity.password)) {
|
||||
issues.push(`identities[${index}].password`);
|
||||
}
|
||||
});
|
||||
|
||||
payload.proxyProfiles?.forEach((profile, index) => {
|
||||
if (isEncryptedCredentialPlaceholder(profile.config.password)) {
|
||||
issues.push(`proxyProfiles[${index}].config.password`);
|
||||
}
|
||||
});
|
||||
|
||||
payload.groupConfigs?.forEach((config, index) => {
|
||||
if (isEncryptedCredentialPlaceholder(config.password)) {
|
||||
issues.push(`groupConfigs[${index}].password`);
|
||||
}
|
||||
if (isEncryptedCredentialPlaceholder(config.telnetPassword)) {
|
||||
issues.push(`groupConfigs[${index}].telnetPassword`);
|
||||
}
|
||||
if (isEncryptedCredentialPlaceholder(config.proxyConfig?.password)) {
|
||||
issues.push(`groupConfigs[${index}].proxyConfig.password`);
|
||||
}
|
||||
});
|
||||
|
||||
return issues;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear device-bound enc:v1 placeholders from a portable sync payload.
|
||||
*
|
||||
* Cloud / backup payloads must carry plaintext secrets (protected by the
|
||||
* master key envelope). If a previous bug uploaded undecryptable local
|
||||
* ciphertext, stripping placeholders lets download restore a usable vault
|
||||
* shell so the user can re-enter credentials instead of looping forever.
|
||||
*/
|
||||
export const stripSyncPayloadEncryptedCredentials = (
|
||||
payload: SyncPayload,
|
||||
): SyncPayload => {
|
||||
const hosts = payload.hosts.map((host) => {
|
||||
const next = { ...host };
|
||||
if (isEncryptedCredentialPlaceholder(next.password)) delete next.password;
|
||||
if (isEncryptedCredentialPlaceholder(next.telnetPassword)) delete next.telnetPassword;
|
||||
if (next.proxyConfig && isEncryptedCredentialPlaceholder(next.proxyConfig.password)) {
|
||||
const { password: _removed, ...proxyRest } = next.proxyConfig;
|
||||
next.proxyConfig = proxyRest;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
const keys = payload.keys.map((key) => {
|
||||
const next = { ...key };
|
||||
if (isEncryptedCredentialPlaceholder(next.privateKey)) next.privateKey = "";
|
||||
if (isEncryptedCredentialPlaceholder(next.passphrase)) delete next.passphrase;
|
||||
return next;
|
||||
});
|
||||
|
||||
const identities = payload.identities?.map((identity) => {
|
||||
if (!isEncryptedCredentialPlaceholder(identity.password)) return identity;
|
||||
const next = { ...identity };
|
||||
delete next.password;
|
||||
return next;
|
||||
});
|
||||
|
||||
const proxyProfiles = payload.proxyProfiles?.map((profile) => {
|
||||
if (!isEncryptedCredentialPlaceholder(profile.config.password)) return profile;
|
||||
const { password: _removed, ...configRest } = profile.config;
|
||||
return { ...profile, config: configRest };
|
||||
});
|
||||
|
||||
const groupConfigs = payload.groupConfigs?.map((config) => {
|
||||
const next = { ...config };
|
||||
if (isEncryptedCredentialPlaceholder(next.password)) delete next.password;
|
||||
if (isEncryptedCredentialPlaceholder(next.telnetPassword)) delete next.telnetPassword;
|
||||
if (next.proxyConfig && isEncryptedCredentialPlaceholder(next.proxyConfig.password)) {
|
||||
const { password: _removed, ...proxyRest } = next.proxyConfig;
|
||||
next.proxyConfig = proxyRest;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
return {
|
||||
...payload,
|
||||
hosts,
|
||||
keys,
|
||||
identities: identities ?? payload.identities,
|
||||
proxyProfiles: proxyProfiles ?? payload.proxyProfiles,
|
||||
groupConfigs: groupConfigs ?? payload.groupConfigs,
|
||||
};
|
||||
};
|
||||
|
||||
const usableCredential = (value: string | undefined): string | undefined => {
|
||||
if (typeof value !== "string" || value.length === 0) return undefined;
|
||||
if (isEncryptedCredentialPlaceholder(value)) return undefined;
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a poisoned secret against preferred then fallback.
|
||||
*
|
||||
* If the preferred *entity* exists, an empty/missing preferred secret is an
|
||||
* explicit deletion and must win over fallback/base. Only when preferred is
|
||||
* also poisoned (or the preferred entity is absent) may we revive from base.
|
||||
*/
|
||||
const healPoisonedCredential = (
|
||||
preferredEntity: unknown,
|
||||
preferredValue: string | undefined,
|
||||
fallbackValue: string | undefined,
|
||||
): string | undefined => {
|
||||
if (preferredEntity) {
|
||||
if (isEncryptedCredentialPlaceholder(preferredValue)) {
|
||||
return usableCredential(fallbackValue);
|
||||
}
|
||||
return usableCredential(preferredValue);
|
||||
}
|
||||
return usableCredential(fallbackValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* Before three-way merge, replace device-bound enc:v1 secrets on `poisoned`
|
||||
* with usable values from `preferred` then `fallback`. Used for both remote
|
||||
* and local sides so a poisoned field cannot win the entity-level merge and
|
||||
* delete a still-usable secret from the other side / base.
|
||||
*
|
||||
* Explicit preferred-side deletions (empty/absent secrets on a present entity)
|
||||
* are authoritative and are not revived from base.
|
||||
*/
|
||||
export const healPoisonedSecretsForMerge = (
|
||||
poisoned: SyncPayload,
|
||||
preferred: SyncPayload,
|
||||
fallback: SyncPayload | null | undefined,
|
||||
): SyncPayload => {
|
||||
const preferredHosts = new Map(preferred.hosts.map((host) => [host.id, host]));
|
||||
const fallbackHosts = new Map((fallback?.hosts ?? []).map((host) => [host.id, host]));
|
||||
const hosts = poisoned.hosts.map((host) => {
|
||||
const preferredHost = preferredHosts.get(host.id);
|
||||
const fallbackHost = fallbackHosts.get(host.id);
|
||||
const next = { ...host };
|
||||
if (isEncryptedCredentialPlaceholder(next.password)) {
|
||||
const healed = healPoisonedCredential(
|
||||
preferredHost,
|
||||
preferredHost?.password,
|
||||
fallbackHost?.password,
|
||||
);
|
||||
if (healed !== undefined) next.password = healed;
|
||||
else delete next.password;
|
||||
}
|
||||
if (isEncryptedCredentialPlaceholder(next.telnetPassword)) {
|
||||
const healed = healPoisonedCredential(
|
||||
preferredHost,
|
||||
preferredHost?.telnetPassword,
|
||||
fallbackHost?.telnetPassword,
|
||||
);
|
||||
if (healed !== undefined) next.telnetPassword = healed;
|
||||
else delete next.telnetPassword;
|
||||
}
|
||||
if (next.proxyConfig && isEncryptedCredentialPlaceholder(next.proxyConfig.password)) {
|
||||
const healed = healPoisonedCredential(
|
||||
preferredHost,
|
||||
preferredHost?.proxyConfig?.password,
|
||||
fallbackHost?.proxyConfig?.password,
|
||||
);
|
||||
if (healed !== undefined) {
|
||||
next.proxyConfig = { ...next.proxyConfig, password: healed };
|
||||
} else {
|
||||
const { password: _removed, ...proxyRest } = next.proxyConfig;
|
||||
next.proxyConfig = proxyRest;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
const preferredKeys = new Map(preferred.keys.map((key) => [key.id, key]));
|
||||
const fallbackKeys = new Map((fallback?.keys ?? []).map((key) => [key.id, key]));
|
||||
const keys = poisoned.keys.map((key) => {
|
||||
const preferredKey = preferredKeys.get(key.id);
|
||||
const fallbackKey = fallbackKeys.get(key.id);
|
||||
const next = { ...key };
|
||||
if (isEncryptedCredentialPlaceholder(next.privateKey)) {
|
||||
next.privateKey = healPoisonedCredential(
|
||||
preferredKey,
|
||||
preferredKey?.privateKey,
|
||||
fallbackKey?.privateKey,
|
||||
) ?? "";
|
||||
}
|
||||
if (isEncryptedCredentialPlaceholder(next.passphrase)) {
|
||||
const healed = healPoisonedCredential(
|
||||
preferredKey,
|
||||
preferredKey?.passphrase,
|
||||
fallbackKey?.passphrase,
|
||||
);
|
||||
if (healed !== undefined) next.passphrase = healed;
|
||||
else delete next.passphrase;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
const preferredIdentities = new Map((preferred.identities ?? []).map((identity) => [identity.id, identity]));
|
||||
const fallbackIdentities = new Map((fallback?.identities ?? []).map((identity) => [identity.id, identity]));
|
||||
const identities = poisoned.identities?.map((identity) => {
|
||||
if (!isEncryptedCredentialPlaceholder(identity.password)) return identity;
|
||||
const preferredIdentity = preferredIdentities.get(identity.id);
|
||||
const healed = healPoisonedCredential(
|
||||
preferredIdentity,
|
||||
preferredIdentity?.password,
|
||||
fallbackIdentities.get(identity.id)?.password,
|
||||
);
|
||||
if (healed !== undefined) return { ...identity, password: healed };
|
||||
const next = { ...identity };
|
||||
delete next.password;
|
||||
return next;
|
||||
});
|
||||
|
||||
const preferredProfiles = new Map((preferred.proxyProfiles ?? []).map((profile) => [profile.id, profile]));
|
||||
const fallbackProfiles = new Map((fallback?.proxyProfiles ?? []).map((profile) => [profile.id, profile]));
|
||||
const proxyProfiles = poisoned.proxyProfiles?.map((profile) => {
|
||||
if (!isEncryptedCredentialPlaceholder(profile.config.password)) return profile;
|
||||
const preferredProfile = preferredProfiles.get(profile.id);
|
||||
const healed = healPoisonedCredential(
|
||||
preferredProfile,
|
||||
preferredProfile?.config.password,
|
||||
fallbackProfiles.get(profile.id)?.config.password,
|
||||
);
|
||||
if (healed !== undefined) {
|
||||
return { ...profile, config: { ...profile.config, password: healed } };
|
||||
}
|
||||
const { password: _removed, ...configRest } = profile.config;
|
||||
return { ...profile, config: configRest };
|
||||
});
|
||||
|
||||
const preferredGroupConfigs = new Map((preferred.groupConfigs ?? []).map((config) => [config.path, config]));
|
||||
const fallbackGroupConfigs = new Map((fallback?.groupConfigs ?? []).map((config) => [config.path, config]));
|
||||
const groupConfigs = poisoned.groupConfigs?.map((config) => {
|
||||
const preferredConfig = preferredGroupConfigs.get(config.path);
|
||||
const fallbackConfig = fallbackGroupConfigs.get(config.path);
|
||||
const next = { ...config };
|
||||
let changed = false;
|
||||
if (isEncryptedCredentialPlaceholder(next.password)) {
|
||||
const healed = healPoisonedCredential(
|
||||
preferredConfig,
|
||||
preferredConfig?.password,
|
||||
fallbackConfig?.password,
|
||||
);
|
||||
if (healed !== undefined) next.password = healed;
|
||||
else delete next.password;
|
||||
changed = true;
|
||||
}
|
||||
if (isEncryptedCredentialPlaceholder(next.telnetPassword)) {
|
||||
const healed = healPoisonedCredential(
|
||||
preferredConfig,
|
||||
preferredConfig?.telnetPassword,
|
||||
fallbackConfig?.telnetPassword,
|
||||
);
|
||||
if (healed !== undefined) next.telnetPassword = healed;
|
||||
else delete next.telnetPassword;
|
||||
changed = true;
|
||||
}
|
||||
if (next.proxyConfig && isEncryptedCredentialPlaceholder(next.proxyConfig.password)) {
|
||||
const healed = healPoisonedCredential(
|
||||
preferredConfig,
|
||||
preferredConfig?.proxyConfig?.password,
|
||||
fallbackConfig?.proxyConfig?.password,
|
||||
);
|
||||
if (healed !== undefined) {
|
||||
next.proxyConfig = { ...next.proxyConfig, password: healed };
|
||||
} else {
|
||||
const { password: _removed, ...proxyRest } = next.proxyConfig;
|
||||
next.proxyConfig = proxyRest;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
return changed ? next : config;
|
||||
});
|
||||
|
||||
return {
|
||||
...poisoned,
|
||||
hosts,
|
||||
keys,
|
||||
identities: identities ?? poisoned.identities,
|
||||
proxyProfiles: proxyProfiles ?? poisoned.proxyProfiles,
|
||||
groupConfigs: groupConfigs ?? poisoned.groupConfigs,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user