| const fs = require("fs"); |
| const crypto = require("crypto"); |
| const { exec } = require("child_process"); |
| const { execWithPassword, isSudoAvailable } = require("../dns/dnsConfig.js"); |
| const { runElevatedPowerShell, quotePs } = require("../winElevated.js"); |
| const { log, err } = require("../logger"); |
|
|
| const IS_WIN = process.platform === "win32"; |
| const IS_MAC = process.platform === "darwin"; |
| const LINUX_CERT_PATHS = [ |
| |
| { dir: "/usr/local/share/ca-certificates", cmd: "update-ca-certificates" }, |
| |
| { dir: "/etc/ca-certificates/trust-source/anchors", cmd: "update-ca-trust" }, |
| |
| { dir: "/etc/pki/ca-trust/source/anchors", cmd: "update-ca-trust" }, |
| |
| { dir: "/etc/pki/trust/anchors", cmd: "update-ca-certificates" } |
| ]; |
|
|
| function getLinuxCertConfig() { |
| for (const config of LINUX_CERT_PATHS) { |
| if (fs.existsSync(config.dir)) { |
| return config; |
| } |
| } |
| |
| return LINUX_CERT_PATHS[0]; |
| } |
| const ROOT_CA_CN = "9Router MITM Root CA"; |
|
|
| |
| function getCertFingerprint(certPath) { |
| const pem = fs.readFileSync(certPath, "utf-8"); |
| const der = Buffer.from(pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, ""), "base64"); |
| return crypto.createHash("sha1").update(der).digest("hex").toUpperCase().match(/.{2}/g).join(":"); |
| } |
|
|
| |
| |
| |
| async function checkCertInstalled(certPath) { |
| if (IS_WIN) return checkCertInstalledWindows(certPath); |
| if (IS_MAC) return checkCertInstalledMac(certPath); |
| return checkCertInstalledLinux(); |
| } |
|
|
| function checkCertInstalledMac(certPath) { |
| return new Promise((resolve) => { |
| try { |
| const fingerprint = getCertFingerprint(certPath).replace(/:/g, ""); |
| |
| exec(`security find-certificate -a -c "${ROOT_CA_CN}" -Z /Library/Keychains/System.keychain 2>/dev/null`, { windowsHide: true }, (error, stdout) => { |
| if (error || !stdout) return resolve(false); |
| const match = new RegExp(`SHA-1 hash:\\s*${fingerprint}`, "i").test(stdout); |
| if (!match) return resolve(false); |
| |
| exec(`security verify-cert -c "${certPath}" -p ssl -k /Library/Keychains/System.keychain 2>/dev/null`, { windowsHide: true }, (err2) => { |
| resolve(!err2); |
| }); |
| }); |
| } catch { |
| resolve(false); |
| } |
| }); |
| } |
|
|
| function checkCertInstalledWindows(certPath) { |
| return new Promise((resolve) => { |
| |
| let fingerprint; |
| try { |
| fingerprint = getCertFingerprint(certPath).replace(/:/g, ""); |
| } catch { |
| return resolve(false); |
| } |
| exec(`certutil -store Root ${fingerprint}`, { windowsHide: true }, (error) => { |
| resolve(!error); |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| async function installCert(sudoPassword, certPath) { |
| if (!fs.existsSync(certPath)) { |
| throw new Error(`Certificate file not found: ${certPath}`); |
| } |
|
|
| const isInstalled = await checkCertInstalled(certPath); |
| if (isInstalled) { |
| log("π Cert: already trusted β
"); |
| return; |
| } |
|
|
| if (IS_WIN) { |
| await installCertWindows(certPath); |
| } else if (IS_MAC) { |
| await installCertMac(sudoPassword, certPath); |
| } else { |
| await installCertLinux(sudoPassword, certPath); |
| } |
| } |
|
|
| async function installCertMac(sudoPassword, certPath) { |
| |
| const deleteOld = `security delete-certificate -c "9Router MITM Root CA" /Library/Keychains/System.keychain 2>/dev/null || true`; |
| const install = `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`; |
| try { |
| await execWithPassword(`${deleteOld} && ${install}`, sudoPassword); |
| log("π Cert: β
installed to system keychain"); |
| } catch (error) { |
| const msg = error.message?.includes("canceled") ? "User canceled authorization" : "Certificate install failed"; |
| throw new Error(msg); |
| } |
| } |
|
|
| async function installCertWindows(certPath) { |
| |
| |
| const script = ` |
| certutil -delstore Root ${quotePs(ROOT_CA_CN)} 2>$null | Out-Null |
| $exit = & certutil -addstore Root ${quotePs(certPath)} 2>&1 |
| if ($LASTEXITCODE -ne 0) { throw "certutil exit $LASTEXITCODE" } |
| `; |
| try { |
| await runElevatedPowerShell(script); |
| log("π Cert: β
installed to Windows Root store"); |
| } catch (e) { |
| throw new Error(`Failed to install certificate: ${e.message}`); |
| } |
| } |
|
|
| |
| |
| |
| async function uninstallCert(sudoPassword, certPath) { |
| const isInstalled = await checkCertInstalled(certPath); |
| if (!isInstalled) { |
| log("π Cert: not found in system store"); |
| return; |
| } |
|
|
| if (IS_WIN) { |
| await uninstallCertWindows(); |
| } else if (IS_MAC) { |
| await uninstallCertMac(sudoPassword, certPath); |
| } else { |
| await uninstallCertLinux(sudoPassword); |
| } |
| } |
|
|
| async function uninstallCertMac(sudoPassword, certPath) { |
| const fingerprint = getCertFingerprint(certPath).replace(/:/g, ""); |
| const command = `security delete-certificate -Z "${fingerprint}" /Library/Keychains/System.keychain`; |
| try { |
| await execWithPassword(command, sudoPassword); |
| log("π Cert: β
uninstalled from system keychain"); |
| } catch (err) { |
| throw new Error("Failed to uninstall certificate"); |
| } |
| } |
|
|
| async function uninstallCertWindows() { |
| |
| const script = `certutil -delstore Root ${quotePs(ROOT_CA_CN)}`; |
| try { |
| await runElevatedPowerShell(script); |
| log("π Cert: β
uninstalled from Windows Root store"); |
| } catch (e) { |
| throw new Error(`Failed to uninstall certificate: ${e.message}`); |
| } |
| } |
|
|
| function checkCertInstalledLinux() { |
| const config = getLinuxCertConfig(); |
| const certFile = `${config.dir}/9router-root-ca.crt`; |
| return Promise.resolve(fs.existsSync(certFile)); |
| } |
|
|
| async function updateNssDatabases(certPath, action = 'add') { |
| const certName = "9Router MITM Root CA"; |
| |
| const script = ` |
| if ! command -v certutil &> /dev/null; then |
| exit 0 |
| fi |
| |
| DIRS="$HOME/.pki/nssdb $HOME/snap/chromium/current/.pki/nssdb" |
| |
| if [ -d "$HOME/.mozilla/firefox" ]; then |
| for profile in "$HOME"/.mozilla/firefox/*/; do |
| if [ -f "\${profile}cert9.db" ] || [ -f "\${profile}cert8.db" ]; then |
| DIRS="$DIRS $profile" |
| fi |
| done |
| fi |
| |
| if [ -d "$HOME/snap/firefox/common/.mozilla/firefox" ]; then |
| for profile in "$HOME"/snap/firefox/common/.mozilla/firefox/*/; do |
| if [ -f "\${profile}cert9.db" ] || [ -f "\${profile}cert8.db" ]; then |
| DIRS="$DIRS $profile" |
| fi |
| done |
| fi |
| |
| for db in $DIRS; do |
| if [ -d "$db" ]; then |
| if [ "${action}" = "add" ]; then |
| certutil -d sql:"$db" -A -t "C,," -n "${certName}" -i "${certPath}" 2>/dev/null || \\ |
| certutil -d "$db" -A -t "C,," -n "${certName}" -i "${certPath}" 2>/dev/null || true |
| else |
| certutil -d sql:"$db" -D -n "${certName}" 2>/dev/null || \\ |
| certutil -d "$db" -D -n "${certName}" 2>/dev/null || true |
| fi |
| fi |
| done |
| `; |
| |
| return new Promise((resolve) => { |
| exec(script, { shell: "/bin/bash" }, () => resolve()); |
| }); |
| } |
|
|
| async function installCertLinux(sudoPassword, certPath) { |
| if (!isSudoAvailable()) { |
| log(`π Cert: cannot install to system store without sudo β trust this file on clients: ${certPath}`); |
| |
| await updateNssDatabases(certPath, 'add'); |
| return; |
| } |
| |
| const config = getLinuxCertConfig(); |
| const destFile = `${config.dir}/9router-root-ca.crt`; |
| |
| |
| const cmd = `cp "${certPath}" "${destFile}" && (${config.cmd} 2>/dev/null || true)`; |
| |
| try { |
| await execWithPassword(cmd, sudoPassword); |
| await updateNssDatabases(certPath, 'add'); |
| log(`π Cert: β
installed to Linux trust store (${config.dir}) and user browser databases`); |
| } catch (error) { |
| throw new Error(`Certificate install failed: ${error.message}`); |
| } |
| } |
|
|
| async function uninstallCertLinux(sudoPassword) { |
| |
| await updateNssDatabases(null, 'delete'); |
|
|
| if (!isSudoAvailable()) { |
| return; |
| } |
| |
| const config = getLinuxCertConfig(); |
| const destFile = `${config.dir}/9router-root-ca.crt`; |
| const cmd = `rm -f "${destFile}" && (${config.cmd} 2>/dev/null || true)`; |
| |
| try { |
| await execWithPassword(cmd, sudoPassword); |
| log("π Cert: β
uninstalled from Linux trust store and user browser databases"); |
| } catch (error) { |
| throw new Error("Failed to uninstall certificate"); |
| } |
| } |
|
|
| module.exports = { installCert, uninstallCert, checkCertInstalled }; |
|
|