sliftutils 1.7.124 → 1.7.126
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/CLAUDE.md +3 -1
- package/bin/derivekey.js +10 -0
- package/bin/portsecuredaemon.js +13 -0
- package/bin/securessh.js +10 -0
- package/bin/setupnotify.js +9 -0
- package/bin/signfiles.js +10 -0
- package/bin/unrevoke.js +9 -0
- package/package.json +14 -3
- package/security/README.md +141 -0
- package/security/authorizedKeys/authorizedKeys.ts +66 -0
- package/security/authorizedKeys/daemon/authLog.ts +117 -0
- package/security/authorizedKeys/daemon/daemon.ts +308 -0
- package/security/authorizedKeys/daemon/git.ts +119 -0
- package/security/authorizedKeys/daemon/notify.ts +25 -0
- package/security/authorizedKeys/daemon/paths.ts +26 -0
- package/security/authorizedKeys/daemon/portsecure.service +19 -0
- package/security/authorizedKeys/daemon/revocation.ts +306 -0
- package/security/authorizedKeys/daemon/rootKeys.ts +135 -0
- package/security/authorizedKeys/daemon/sshdConfig.ts +85 -0
- package/security/authorizedKeys/daemon/state.ts +108 -0
- package/security/authorizedKeys/daemon/trust.ts +291 -0
- package/security/authorizedKeys/daemon/userKeys.ts +76 -0
- package/security/authorizedKeys/dist/authorizedKeys.ts.cache +73 -0
- package/security/authorizedKeys/dist/revokeSource.ts.cache +44 -0
- package/security/authorizedKeys/dist/secureSSH.ts.cache +552 -0
- package/security/authorizedKeys/dist/sources.ts.cache +24 -0
- package/security/authorizedKeys/dist/unrevoke.ts.cache +145 -0
- package/security/authorizedKeys/revokeSource.ts +40 -0
- package/security/authorizedKeys/secureSSH.ts +613 -0
- package/security/authorizedKeys/sources.ts +20 -0
- package/security/authorizedKeys/unrevoke.ts +149 -0
- package/security/helpers/dist/paths.ts.cache +28 -0
- package/security/helpers/dist/remoteSSH.ts.cache +90 -0
- package/security/helpers/dist/spawn.ts.cache +34 -0
- package/security/helpers/paths.ts +20 -0
- package/security/helpers/remoteSSH.ts +95 -0
- package/security/helpers/spawn.ts +36 -0
- package/security/keys/deriveKey.ts +72 -0
- package/security/keys/dist/deriveKey.ts.cache +72 -0
- package/security/keys/dist/sshKeyFile.ts.cache +153 -0
- package/security/keys/sshKeyFile.ts +156 -0
- package/security/notifications/discord.ts +190 -0
- package/security/notifications/dist/discord.ts.cache +180 -0
- package/security/notifications/remoteWebhook.ts +85 -0
- package/security/notifications/setupNotify.ts +29 -0
- package/security/signedFiles/dist/manifest.ts.cache +68 -0
- package/security/signedFiles/dist/signFiles.ts.cache +146 -0
- package/security/signedFiles/manifest.ts +69 -0
- package/security/signedFiles/signFiles.ts +151 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +17 -20
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { runPromise } from "socket-function/src/runPromise";
|
|
5
|
+
import { keyFingerprint, normalizeKeys } from "./authorizedKeys";
|
|
6
|
+
import { revokeRepoURL } from "./revokeSource";
|
|
7
|
+
import { readRepoKeys } from "./authorizedKeys";
|
|
8
|
+
import { expandHome } from "../helpers/paths";
|
|
9
|
+
import { spawnPromise } from "../helpers/spawn";
|
|
10
|
+
|
|
11
|
+
const UNREVOKES_DIR = "unrevoked";
|
|
12
|
+
const REVOCATIONS_DIR = "revocations";
|
|
13
|
+
const USAGE = `Usage: yarn unrevoke [keys-repo]
|
|
14
|
+
|
|
15
|
+
Run this in a keys repo, or name one. It reads that repo's revoke repo and writes one unrevoke file
|
|
16
|
+
naming every revocation in it, so the keys are accepted again once each machine's hour long wait
|
|
17
|
+
passes.
|
|
18
|
+
|
|
19
|
+
Keys that were revoked should normally be deleted from the repo instead. Unrevoking only matters
|
|
20
|
+
for a key you still want.`;
|
|
21
|
+
|
|
22
|
+
export type Revocation = {
|
|
23
|
+
revocationId: string;
|
|
24
|
+
fingerprint: string;
|
|
25
|
+
key?: string;
|
|
26
|
+
revokedAt?: string;
|
|
27
|
+
revokedBy?: string;
|
|
28
|
+
attempt?: { ip?: string; user?: string; required?: string };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Every revocation the revoke repo lists. Cloned read only into a temp directory, with whatever
|
|
32
|
+
credentials this machine already has - the derived deploy key is for servers, and a person
|
|
33
|
+
running this has their own access to the repo. */
|
|
34
|
+
export async function readRemoteRevocations(config: { sourceURL: string }) {
|
|
35
|
+
let { sourceURL } = config;
|
|
36
|
+
let temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "unrevoke-"));
|
|
37
|
+
let repoPath = path.join(temporaryDirectory, "repo");
|
|
38
|
+
let clone = await spawnPromise({
|
|
39
|
+
command: "git",
|
|
40
|
+
args: ["clone", "--depth", "1", revokeRepoURL(sourceURL), repoPath],
|
|
41
|
+
});
|
|
42
|
+
if (clone.status !== 0) {
|
|
43
|
+
await fs.rm(temporaryDirectory, { recursive: true, force: true });
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Expected to be able to read ${revokeRepoURL(sourceURL)}.\n`
|
|
46
|
+
+ `${(clone.stdout + clone.stderr).trim()}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
let revocations: Revocation[] = [];
|
|
50
|
+
let directory = path.join(repoPath, REVOCATIONS_DIR);
|
|
51
|
+
try {
|
|
52
|
+
for (let name of (await fs.readdir(directory)).sort()) {
|
|
53
|
+
if (name.endsWith(".json")) {
|
|
54
|
+
revocations.push(JSON.parse(await fs.readFile(path.join(directory, name), "utf8")));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// No revocations directory at all just means nothing has ever been revoked.
|
|
59
|
+
}
|
|
60
|
+
await fs.rm(temporaryDirectory, { recursive: true, force: true });
|
|
61
|
+
return revocations;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Which keys in a repo are revoked. What signfiles and securessh refuse over. */
|
|
65
|
+
export async function revokedKeysInRepo(config: { repoPath: string; sourceURL: string }) {
|
|
66
|
+
let { repoPath, sourceURL } = config;
|
|
67
|
+
let revocations = await readRemoteRevocations({ sourceURL });
|
|
68
|
+
let revokedFingerprints = new Set(revocations.map(revocation => revocation.fingerprint));
|
|
69
|
+
let unrevoked = new Set<string>();
|
|
70
|
+
try {
|
|
71
|
+
let directory = path.join(repoPath, UNREVOKES_DIR);
|
|
72
|
+
for (let name of await fs.readdir(directory)) {
|
|
73
|
+
if (!name.endsWith(".json")) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let parsed = JSON.parse(await fs.readFile(path.join(directory, name), "utf8"));
|
|
77
|
+
for (let revocationId of parsed.revocationIds || []) {
|
|
78
|
+
unrevoked.add(revocationId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Nothing has been unrevoked.
|
|
83
|
+
}
|
|
84
|
+
let stillRevoked = revocations.filter(revocation => !unrevoked.has(revocation.revocationId));
|
|
85
|
+
let keys = normalizeKeys(await fs.readFile(path.join(repoPath, "authorized_keys"), "utf8").catch(() => ""));
|
|
86
|
+
return stillRevoked
|
|
87
|
+
.filter(revocation => keys.some(key => keyFingerprint(key) === revocation.fingerprint))
|
|
88
|
+
.map(revocation => ({ revocation, key: keys.find(key => keyFingerprint(key) === revocation.fingerprint) || "" }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The repo named, or the one we are standing in. Checked the same way securessh checks it: a git
|
|
92
|
+
repo that actually holds keys and has an origin to clone from. */
|
|
93
|
+
async function resolveKeysRepo(named: string | undefined) {
|
|
94
|
+
let cwd = named && expandHome(named) || undefined;
|
|
95
|
+
let repoPath = (await runPromise("git rev-parse --show-toplevel", { quiet: true, cwd })).trim();
|
|
96
|
+
if (!repoPath) {
|
|
97
|
+
throw new Error(`Expected ${named || "the current directory"} to be inside a git repo, it is not.\n${USAGE}`);
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await readRepoKeys(repoPath);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
throw new Error(`Expected ${repoPath} to be a keys repo, it holds no keys.\n${e}\n${USAGE}`);
|
|
103
|
+
}
|
|
104
|
+
let originURL = (await runPromise("git remote get-url origin", { quiet: true, cwd: repoPath })).trim();
|
|
105
|
+
if (!originURL) {
|
|
106
|
+
throw new Error(`Expected ${repoPath} to have an origin remote, it has none.\n${USAGE}`);
|
|
107
|
+
}
|
|
108
|
+
return { repoPath, originURL };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function main() {
|
|
112
|
+
let argv = process.argv.slice(2);
|
|
113
|
+
if (argv.length > 1) {
|
|
114
|
+
throw new Error(`Expected at most a keys repo, was ${argv.length} argument(s)\n${USAGE}`);
|
|
115
|
+
}
|
|
116
|
+
let { repoPath, originURL } = await resolveKeysRepo(argv[0]);
|
|
117
|
+
|
|
118
|
+
let revocations = await readRemoteRevocations({ sourceURL: originURL });
|
|
119
|
+
if (!revocations.length) {
|
|
120
|
+
console.log(`${revokeRepoURL(originURL)} lists no revocations, so there is nothing to undo.`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let directory = path.join(repoPath, UNREVOKES_DIR);
|
|
125
|
+
await fs.mkdir(directory, { recursive: true });
|
|
126
|
+
let stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
127
|
+
let unrevokeId = `${stamp}-unrevoke`;
|
|
128
|
+
await fs.writeFile(path.join(directory, `${unrevokeId}.json`), JSON.stringify({
|
|
129
|
+
unrevokeId,
|
|
130
|
+
createdAt: new Date().toISOString(),
|
|
131
|
+
// Named one by one rather than as a blanket "allow everything again", so this file only
|
|
132
|
+
// ever undoes the revocations that existed when it was written.
|
|
133
|
+
revocationIds: revocations.map(revocation => revocation.revocationId),
|
|
134
|
+
revocations: revocations.map(revocation => ({
|
|
135
|
+
revocationId: revocation.revocationId,
|
|
136
|
+
fingerprint: revocation.fingerprint,
|
|
137
|
+
revokedAt: revocation.revokedAt,
|
|
138
|
+
revokedBy: revocation.revokedBy,
|
|
139
|
+
attempt: revocation.attempt,
|
|
140
|
+
})),
|
|
141
|
+
}, undefined, 4) + "\n");
|
|
142
|
+
|
|
143
|
+
console.log(`Wrote ${UNREVOKES_DIR}/${unrevokeId}.json covering ${revocations.length} revocation(s):`);
|
|
144
|
+
for (let revocation of revocations) {
|
|
145
|
+
console.log(` ${revocation.fingerprint} revoked by ${revocation.revokedBy || "?"} from ${revocation.attempt?.ip || "?"}`);
|
|
146
|
+
}
|
|
147
|
+
console.log(`Each machine waits an hour after seeing it before the keys work again.`);
|
|
148
|
+
console.log(`Sign and publish it with:\n yarn signfiles git`);
|
|
149
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
6
|
+
//exports.expandHome = void 0;
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/** Windows shells do not expand ~ themselves, and hunting down your home folder by hand is
|
|
10
|
+
annoying, so we expand it on every platform. Both separators are accepted, because a Windows
|
|
11
|
+
user may type either one. */
|
|
12
|
+
function expandHome(filePath) {
|
|
13
|
+
if (filePath === "~") {
|
|
14
|
+
return os_1.default.homedir();
|
|
15
|
+
}
|
|
16
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
17
|
+
return path_1.default.join(os_1.default.homedir(), filePath.slice(2));
|
|
18
|
+
}
|
|
19
|
+
if (filePath.startsWith("~")) {
|
|
20
|
+
// ~otheruser needs an account database we cannot read portably, and quietly resolving it
|
|
21
|
+
// to the wrong home would be worse than refusing.
|
|
22
|
+
throw new Error(`Expected ~ or an ordinary path, was ${filePath}. Referring to another user's home with ~name is not supported.`);
|
|
23
|
+
}
|
|
24
|
+
return path_1.default.resolve(filePath);
|
|
25
|
+
}
|
|
26
|
+
exports.expandHome = expandHome;
|
|
27
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGF0aHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJwYXRocy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSw0Q0FBb0I7QUFDcEIsZ0RBQXdCO0FBRXhCOztnQ0FFZ0M7QUFDaEMsU0FBZ0IsVUFBVSxDQUFDLFFBQWdCO0lBQ3ZDLElBQUksUUFBUSxLQUFLLEdBQUcsRUFBRSxDQUFDO1FBQ25CLE9BQU8sWUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQ3hCLENBQUM7SUFDRCxJQUFJLFFBQVEsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLElBQUksUUFBUSxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQzFELE9BQU8sY0FBSSxDQUFDLElBQUksQ0FBQyxZQUFFLENBQUMsT0FBTyxFQUFFLEVBQUUsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3RELENBQUM7SUFDRCxJQUFJLFFBQVEsQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUMzQix5RkFBeUY7UUFDekYsa0RBQWtEO1FBQ2xELE1BQU0sSUFBSSxLQUFLLENBQUMsdUNBQXVDLFFBQVEsaUVBQWlFLENBQUMsQ0FBQztJQUN0SSxDQUFDO0lBQ0QsT0FBTyxjQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0FBQ2xDLENBQUM7QUFiRCxnQ0FhQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBvcyBmcm9tIFwib3NcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbi8qKiBXaW5kb3dzIHNoZWxscyBkbyBub3QgZXhwYW5kIH4gdGhlbXNlbHZlcywgYW5kIGh1bnRpbmcgZG93biB5b3VyIGhvbWUgZm9sZGVyIGJ5IGhhbmQgaXNcbiAgICBhbm5veWluZywgc28gd2UgZXhwYW5kIGl0IG9uIGV2ZXJ5IHBsYXRmb3JtLiBCb3RoIHNlcGFyYXRvcnMgYXJlIGFjY2VwdGVkLCBiZWNhdXNlIGEgV2luZG93c1xuICAgIHVzZXIgbWF5IHR5cGUgZWl0aGVyIG9uZS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBleHBhbmRIb21lKGZpbGVQYXRoOiBzdHJpbmcpIHtcbiAgICBpZiAoZmlsZVBhdGggPT09IFwiflwiKSB7XG4gICAgICAgIHJldHVybiBvcy5ob21lZGlyKCk7XG4gICAgfVxuICAgIGlmIChmaWxlUGF0aC5zdGFydHNXaXRoKFwifi9cIikgfHwgZmlsZVBhdGguc3RhcnRzV2l0aChcIn5cXFxcXCIpKSB7XG4gICAgICAgIHJldHVybiBwYXRoLmpvaW4ob3MuaG9tZWRpcigpLCBmaWxlUGF0aC5zbGljZSgyKSk7XG4gICAgfVxuICAgIGlmIChmaWxlUGF0aC5zdGFydHNXaXRoKFwiflwiKSkge1xuICAgICAgICAvLyB+b3RoZXJ1c2VyIG5lZWRzIGFuIGFjY291bnQgZGF0YWJhc2Ugd2UgY2Fubm90IHJlYWQgcG9ydGFibHksIGFuZCBxdWlldGx5IHJlc29sdmluZyBpdFxuICAgICAgICAvLyB0byB0aGUgd3JvbmcgaG9tZSB3b3VsZCBiZSB3b3JzZSB0aGFuIHJlZnVzaW5nLlxuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIH4gb3IgYW4gb3JkaW5hcnkgcGF0aCwgd2FzICR7ZmlsZVBhdGh9LiBSZWZlcnJpbmcgdG8gYW5vdGhlciB1c2VyJ3MgaG9tZSB3aXRoIH5uYW1lIGlzIG5vdCBzdXBwb3J0ZWQuYCk7XG4gICAgfVxuICAgIHJldHVybiBwYXRoLnJlc29sdmUoZmlsZVBhdGgpO1xufVxuIl19
|
|
28
|
+
/* _JS_SOURCE_HASH = "aa5698729d5def416d177e211ac514eed7d8c8fcb796c68dcc520ddf0d83526b"; */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
|
+
//exports.remoteCommandExists = exports.writeRemoteFile = exports.readRemoteFile = exports.runOverSSH = exports.SUDO_PREAMBLE = void 0;
|
|
4
|
+
const spawn_1 = require("./spawn");
|
|
5
|
+
const SSH_CONNECT_TIMEOUT = 10;
|
|
6
|
+
const EXISTS_MARKER = "__PORTSECURE_EXISTS__";
|
|
7
|
+
const MISSING_MARKER = "__PORTSECURE_MISSING__";
|
|
8
|
+
const MAX_ERROR_BODY_LENGTH = 500;
|
|
9
|
+
// /etc is root owned, so fall back to sudo whenever the SSH user is not root.
|
|
10
|
+
exports.SUDO_PREAMBLE = `SUDO=""; if [ "$(id -u)" -ne 0 ]; then SUDO="sudo -n"; fi`;
|
|
11
|
+
/** The host string is handed to ssh untouched. Users, keys and ports belong in the caller's ssh
|
|
12
|
+
config, so BatchMode makes a missing setup fail immediately instead of prompting. */
|
|
13
|
+
async function runOverSSH(config) {
|
|
14
|
+
let { host, script, input, allowFailure } = config;
|
|
15
|
+
let result = await (0, spawn_1.spawnPromise)({
|
|
16
|
+
command: "ssh",
|
|
17
|
+
args: [
|
|
18
|
+
"-o", "BatchMode=yes",
|
|
19
|
+
"-o", `ConnectTimeout=${SSH_CONNECT_TIMEOUT}`,
|
|
20
|
+
host,
|
|
21
|
+
script,
|
|
22
|
+
],
|
|
23
|
+
input,
|
|
24
|
+
inheritStderr: !allowFailure,
|
|
25
|
+
});
|
|
26
|
+
if (result.error) {
|
|
27
|
+
throw new Error(`Expected ssh to run against ${host}, failed with ${result.error.message}`);
|
|
28
|
+
}
|
|
29
|
+
if (result.status !== 0 && !allowFailure) {
|
|
30
|
+
throw new Error(`Expected ssh to ${host} to exit 0, was ${result.status}.`
|
|
31
|
+
+ ` Non-interactive ssh access to ${host} has to work on its own - fix it in your ssh config.\n`
|
|
32
|
+
+ `${(result.stdout + result.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`);
|
|
33
|
+
}
|
|
34
|
+
return { stdout: result.stdout || "", stderr: result.stderr || "", status: result.status };
|
|
35
|
+
}
|
|
36
|
+
exports.runOverSSH = runOverSSH;
|
|
37
|
+
/** Returns undefined when the file does not exist, so a missing file reads differently from an
|
|
38
|
+
empty one. */
|
|
39
|
+
async function readRemoteFile(config) {
|
|
40
|
+
let { host, filePath } = config;
|
|
41
|
+
let output = (await runOverSSH({
|
|
42
|
+
host,
|
|
43
|
+
script: `${exports.SUDO_PREAMBLE}
|
|
44
|
+
if $SUDO test -f "${filePath}"; then
|
|
45
|
+
echo "${EXISTS_MARKER}"
|
|
46
|
+
$SUDO cat "${filePath}"
|
|
47
|
+
else
|
|
48
|
+
echo "${MISSING_MARKER}"
|
|
49
|
+
fi`,
|
|
50
|
+
})).stdout;
|
|
51
|
+
let newlineIndex = output.indexOf("\n");
|
|
52
|
+
let marker = output.slice(0, newlineIndex).trim();
|
|
53
|
+
if (marker === MISSING_MARKER) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (marker !== EXISTS_MARKER) {
|
|
57
|
+
throw new Error(`Expected ${EXISTS_MARKER} or ${MISSING_MARKER} from ${host}, was ${output.slice(0, MAX_ERROR_BODY_LENGTH)}`);
|
|
58
|
+
}
|
|
59
|
+
return output.slice(newlineIndex + 1);
|
|
60
|
+
}
|
|
61
|
+
exports.readRemoteFile = readRemoteFile;
|
|
62
|
+
async function writeRemoteFile(config) {
|
|
63
|
+
let { host, filePath, contents, fileMode, directoryMode } = config;
|
|
64
|
+
let directory = filePath.slice(0, filePath.lastIndexOf("/"));
|
|
65
|
+
await runOverSSH({
|
|
66
|
+
host,
|
|
67
|
+
// Deploying from Windows must not carry CRLF onto the target, where it breaks systemd
|
|
68
|
+
// units and makes private keys unreadable to ssh.
|
|
69
|
+
input: contents.replace(/\r\n/g, "\n"),
|
|
70
|
+
script: `${exports.SUDO_PREAMBLE}
|
|
71
|
+
set -e
|
|
72
|
+
$SUDO mkdir -p "${directory}"
|
|
73
|
+
$SUDO chmod ${directoryMode} "${directory}"
|
|
74
|
+
$SUDO tee "${filePath}" > /dev/null
|
|
75
|
+
$SUDO chmod ${fileMode} "${filePath}"`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
exports.writeRemoteFile = writeRemoteFile;
|
|
79
|
+
async function remoteCommandExists(config) {
|
|
80
|
+
let { host, command } = config;
|
|
81
|
+
let result = await runOverSSH({
|
|
82
|
+
host,
|
|
83
|
+
script: `command -v "${command}" > /dev/null 2>&1 && echo yes || echo no`,
|
|
84
|
+
allowFailure: true,
|
|
85
|
+
});
|
|
86
|
+
return result.stdout.trim() === "yes";
|
|
87
|
+
}
|
|
88
|
+
exports.remoteCommandExists = remoteCommandExists;
|
|
89
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVtb3RlU1NILmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicmVtb3RlU1NILnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLG1DQUF1QztBQUV2QyxNQUFNLG1CQUFtQixHQUFHLEVBQUUsQ0FBQztBQUMvQixNQUFNLGFBQWEsR0FBRyx1QkFBdUIsQ0FBQztBQUM5QyxNQUFNLGNBQWMsR0FBRyx3QkFBd0IsQ0FBQztBQUNoRCxNQUFNLHFCQUFxQixHQUFHLEdBQUcsQ0FBQztBQUVsQyw4RUFBOEU7QUFDakUsUUFBQSxhQUFhLEdBQUcsMkRBQTJELENBQUM7QUFFekY7d0ZBQ3dGO0FBQ2pGLEtBQUssVUFBVSxVQUFVLENBQUMsTUFBZ0Y7SUFDN0csSUFBSSxFQUFFLElBQUksRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLFlBQVksRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUNuRCxJQUFJLE1BQU0sR0FBRyxNQUFNLElBQUEsb0JBQVksRUFBQztRQUM1QixPQUFPLEVBQUUsS0FBSztRQUNkLElBQUksRUFBRTtZQUNGLElBQUksRUFBRSxlQUFlO1lBQ3JCLElBQUksRUFBRSxrQkFBa0IsbUJBQW1CLEVBQUU7WUFDN0MsSUFBSTtZQUNKLE1BQU07U0FDVDtRQUNELEtBQUs7UUFDTCxhQUFhLEVBQUUsQ0FBQyxZQUFZO0tBQy9CLENBQUMsQ0FBQztJQUNILElBQUksTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ2YsTUFBTSxJQUFJLEtBQUssQ0FBQywrQkFBK0IsSUFBSSxpQkFBaUIsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDO0lBQ2hHLENBQUM7SUFDRCxJQUFJLE1BQU0sQ0FBQyxNQUFNLEtBQUssQ0FBQyxJQUFJLENBQUMsWUFBWSxFQUFFLENBQUM7UUFDdkMsTUFBTSxJQUFJLEtBQUssQ0FDWCxtQkFBbUIsSUFBSSxtQkFBbUIsTUFBTSxDQUFDLE1BQU0sR0FBRztjQUN4RCxrQ0FBa0MsSUFBSSx3REFBd0Q7Y0FDOUYsR0FBRyxDQUFDLE1BQU0sQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUscUJBQXFCLENBQUMsRUFBRSxDQUNoRixDQUFDO0lBQ04sQ0FBQztJQUNELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDLE1BQU0sSUFBSSxFQUFFLEVBQUUsTUFBTSxFQUFFLE1BQU0sQ0FBQyxNQUFNLElBQUksRUFBRSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUM7QUFDL0YsQ0FBQztBQXhCRCxnQ0F3QkM7QUFFRDtpQkFDaUI7QUFDVixLQUFLLFVBQVUsY0FBYyxDQUFDLE1BQTBDO0lBQzNFLElBQUksRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLEdBQUcsTUFBTSxDQUFDO0lBQ2hDLElBQUksTUFBTSxHQUFHLENBQUMsTUFBTSxVQUFVLENBQUM7UUFDM0IsSUFBSTtRQUNKLE1BQU0sRUFBRSxHQUFHLHFCQUFhO29CQUNaLFFBQVE7WUFDaEIsYUFBYTtpQkFDUixRQUFROztZQUViLGNBQWM7R0FDdkI7S0FDRSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDWCxJQUFJLFlBQVksR0FBRyxNQUFNLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3hDLElBQUksTUFBTSxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLFlBQVksQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQ2xELElBQUksTUFBTSxLQUFLLGNBQWMsRUFBRSxDQUFDO1FBQzVCLE9BQU8sU0FBUyxDQUFDO0lBQ3JCLENBQUM7SUFDRCxJQUFJLE1BQU0sS0FBSyxhQUFhLEVBQUUsQ0FBQztRQUMzQixNQUFNLElBQUksS0FBSyxDQUFDLFlBQVksYUFBYSxPQUFPLGNBQWMsU0FBUyxJQUFJLFNBQVMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUscUJBQXFCLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDbEksQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxZQUFZLEdBQUcsQ0FBQyxDQUFDLENBQUM7QUFDMUMsQ0FBQztBQXJCRCx3Q0FxQkM7QUFFTSxLQUFLLFVBQVUsZUFBZSxDQUFDLE1BTXJDO0lBQ0csSUFBSSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxhQUFhLEVBQUUsR0FBRyxNQUFNLENBQUM7SUFDbkUsSUFBSSxTQUFTLEdBQUcsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsUUFBUSxDQUFDLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQzdELE1BQU0sVUFBVSxDQUFDO1FBQ2IsSUFBSTtRQUNKLHNGQUFzRjtRQUN0RixrREFBa0Q7UUFDbEQsS0FBSyxFQUFFLFFBQVEsQ0FBQyxPQUFPLENBQUMsT0FBTyxFQUFFLElBQUksQ0FBQztRQUN0QyxNQUFNLEVBQUUsR0FBRyxxQkFBYTs7a0JBRWQsU0FBUztjQUNiLGFBQWEsS0FBSyxTQUFTO2FBQzVCLFFBQVE7Y0FDUCxRQUFRLEtBQUssUUFBUSxHQUFHO0tBQ2pDLENBQUMsQ0FBQztBQUNQLENBQUM7QUFyQkQsMENBcUJDO0FBRU0sS0FBSyxVQUFVLG1CQUFtQixDQUFDLE1BQXlDO0lBQy9FLElBQUksRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLEdBQUcsTUFBTSxDQUFDO0lBQy9CLElBQUksTUFBTSxHQUFHLE1BQU0sVUFBVSxDQUFDO1FBQzFCLElBQUk7UUFDSixNQUFNLEVBQUUsZUFBZSxPQUFPLDJDQUEyQztRQUN6RSxZQUFZLEVBQUUsSUFBSTtLQUNyQixDQUFDLENBQUM7SUFDSCxPQUFPLE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLEtBQUssS0FBSyxDQUFDO0FBQzFDLENBQUM7QUFSRCxrREFRQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHNwYXduUHJvbWlzZSB9IGZyb20gXCIuL3NwYXduXCI7XG5cbmNvbnN0IFNTSF9DT05ORUNUX1RJTUVPVVQgPSAxMDtcbmNvbnN0IEVYSVNUU19NQVJLRVIgPSBcIl9fUE9SVFNFQ1VSRV9FWElTVFNfX1wiO1xuY29uc3QgTUlTU0lOR19NQVJLRVIgPSBcIl9fUE9SVFNFQ1VSRV9NSVNTSU5HX19cIjtcbmNvbnN0IE1BWF9FUlJPUl9CT0RZX0xFTkdUSCA9IDUwMDtcblxuLy8gL2V0YyBpcyByb290IG93bmVkLCBzbyBmYWxsIGJhY2sgdG8gc3VkbyB3aGVuZXZlciB0aGUgU1NIIHVzZXIgaXMgbm90IHJvb3QuXG5leHBvcnQgY29uc3QgU1VET19QUkVBTUJMRSA9IGBTVURPPVwiXCI7IGlmIFsgXCIkKGlkIC11KVwiIC1uZSAwIF07IHRoZW4gU1VETz1cInN1ZG8gLW5cIjsgZmlgO1xuXG4vKiogVGhlIGhvc3Qgc3RyaW5nIGlzIGhhbmRlZCB0byBzc2ggdW50b3VjaGVkLiBVc2Vycywga2V5cyBhbmQgcG9ydHMgYmVsb25nIGluIHRoZSBjYWxsZXIncyBzc2hcbiAgICBjb25maWcsIHNvIEJhdGNoTW9kZSBtYWtlcyBhIG1pc3Npbmcgc2V0dXAgZmFpbCBpbW1lZGlhdGVseSBpbnN0ZWFkIG9mIHByb21wdGluZy4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBydW5PdmVyU1NIKGNvbmZpZzogeyBob3N0OiBzdHJpbmc7IHNjcmlwdDogc3RyaW5nOyBpbnB1dD86IHN0cmluZzsgYWxsb3dGYWlsdXJlPzogYm9vbGVhbiB9KSB7XG4gICAgbGV0IHsgaG9zdCwgc2NyaXB0LCBpbnB1dCwgYWxsb3dGYWlsdXJlIH0gPSBjb25maWc7XG4gICAgbGV0IHJlc3VsdCA9IGF3YWl0IHNwYXduUHJvbWlzZSh7XG4gICAgICAgIGNvbW1hbmQ6IFwic3NoXCIsXG4gICAgICAgIGFyZ3M6IFtcbiAgICAgICAgICAgIFwiLW9cIiwgXCJCYXRjaE1vZGU9eWVzXCIsXG4gICAgICAgICAgICBcIi1vXCIsIGBDb25uZWN0VGltZW91dD0ke1NTSF9DT05ORUNUX1RJTUVPVVR9YCxcbiAgICAgICAgICAgIGhvc3QsXG4gICAgICAgICAgICBzY3JpcHQsXG4gICAgICAgIF0sXG4gICAgICAgIGlucHV0LFxuICAgICAgICBpbmhlcml0U3RkZXJyOiAhYWxsb3dGYWlsdXJlLFxuICAgIH0pO1xuICAgIGlmIChyZXN1bHQuZXJyb3IpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBzc2ggdG8gcnVuIGFnYWluc3QgJHtob3N0fSwgZmFpbGVkIHdpdGggJHtyZXN1bHQuZXJyb3IubWVzc2FnZX1gKTtcbiAgICB9XG4gICAgaWYgKHJlc3VsdC5zdGF0dXMgIT09IDAgJiYgIWFsbG93RmFpbHVyZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgICAgICBgRXhwZWN0ZWQgc3NoIHRvICR7aG9zdH0gdG8gZXhpdCAwLCB3YXMgJHtyZXN1bHQuc3RhdHVzfS5gXG4gICAgICAgICAgICArIGAgTm9uLWludGVyYWN0aXZlIHNzaCBhY2Nlc3MgdG8gJHtob3N0fSBoYXMgdG8gd29yayBvbiBpdHMgb3duIC0gZml4IGl0IGluIHlvdXIgc3NoIGNvbmZpZy5cXG5gXG4gICAgICAgICAgICArIGAkeyhyZXN1bHQuc3Rkb3V0ICsgcmVzdWx0LnN0ZGVycikudHJpbSgpLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCl9YFxuICAgICAgICApO1xuICAgIH1cbiAgICByZXR1cm4geyBzdGRvdXQ6IHJlc3VsdC5zdGRvdXQgfHwgXCJcIiwgc3RkZXJyOiByZXN1bHQuc3RkZXJyIHx8IFwiXCIsIHN0YXR1czogcmVzdWx0LnN0YXR1cyB9O1xufVxuXG4vKiogUmV0dXJucyB1bmRlZmluZWQgd2hlbiB0aGUgZmlsZSBkb2VzIG5vdCBleGlzdCwgc28gYSBtaXNzaW5nIGZpbGUgcmVhZHMgZGlmZmVyZW50bHkgZnJvbSBhblxuICAgIGVtcHR5IG9uZS4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZWFkUmVtb3RlRmlsZShjb25maWc6IHsgaG9zdDogc3RyaW5nOyBmaWxlUGF0aDogc3RyaW5nIH0pIHtcbiAgICBsZXQgeyBob3N0LCBmaWxlUGF0aCB9ID0gY29uZmlnO1xuICAgIGxldCBvdXRwdXQgPSAoYXdhaXQgcnVuT3ZlclNTSCh7XG4gICAgICAgIGhvc3QsXG4gICAgICAgIHNjcmlwdDogYCR7U1VET19QUkVBTUJMRX1cbmlmICRTVURPIHRlc3QgLWYgXCIke2ZpbGVQYXRofVwiOyB0aGVuXG4gICAgZWNobyBcIiR7RVhJU1RTX01BUktFUn1cIlxuICAgICRTVURPIGNhdCBcIiR7ZmlsZVBhdGh9XCJcbmVsc2VcbiAgICBlY2hvIFwiJHtNSVNTSU5HX01BUktFUn1cIlxuZmlgLFxuICAgIH0pKS5zdGRvdXQ7XG4gICAgbGV0IG5ld2xpbmVJbmRleCA9IG91dHB1dC5pbmRleE9mKFwiXFxuXCIpO1xuICAgIGxldCBtYXJrZXIgPSBvdXRwdXQuc2xpY2UoMCwgbmV3bGluZUluZGV4KS50cmltKCk7XG4gICAgaWYgKG1hcmtlciA9PT0gTUlTU0lOR19NQVJLRVIpIHtcbiAgICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG4gICAgaWYgKG1hcmtlciAhPT0gRVhJU1RTX01BUktFUikge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkICR7RVhJU1RTX01BUktFUn0gb3IgJHtNSVNTSU5HX01BUktFUn0gZnJvbSAke2hvc3R9LCB3YXMgJHtvdXRwdXQuc2xpY2UoMCwgTUFYX0VSUk9SX0JPRFlfTEVOR1RIKX1gKTtcbiAgICB9XG4gICAgcmV0dXJuIG91dHB1dC5zbGljZShuZXdsaW5lSW5kZXggKyAxKTtcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHdyaXRlUmVtb3RlRmlsZShjb25maWc6IHtcbiAgICBob3N0OiBzdHJpbmc7XG4gICAgZmlsZVBhdGg6IHN0cmluZztcbiAgICBjb250ZW50czogc3RyaW5nO1xuICAgIGZpbGVNb2RlOiBzdHJpbmc7XG4gICAgZGlyZWN0b3J5TW9kZTogc3RyaW5nO1xufSkge1xuICAgIGxldCB7IGhvc3QsIGZpbGVQYXRoLCBjb250ZW50cywgZmlsZU1vZGUsIGRpcmVjdG9yeU1vZGUgfSA9IGNvbmZpZztcbiAgICBsZXQgZGlyZWN0b3J5ID0gZmlsZVBhdGguc2xpY2UoMCwgZmlsZVBhdGgubGFzdEluZGV4T2YoXCIvXCIpKTtcbiAgICBhd2FpdCBydW5PdmVyU1NIKHtcbiAgICAgICAgaG9zdCxcbiAgICAgICAgLy8gRGVwbG95aW5nIGZyb20gV2luZG93cyBtdXN0IG5vdCBjYXJyeSBDUkxGIG9udG8gdGhlIHRhcmdldCwgd2hlcmUgaXQgYnJlYWtzIHN5c3RlbWRcbiAgICAgICAgLy8gdW5pdHMgYW5kIG1ha2VzIHByaXZhdGUga2V5cyB1bnJlYWRhYmxlIHRvIHNzaC5cbiAgICAgICAgaW5wdXQ6IGNvbnRlbnRzLnJlcGxhY2UoL1xcclxcbi9nLCBcIlxcblwiKSxcbiAgICAgICAgc2NyaXB0OiBgJHtTVURPX1BSRUFNQkxFfVxuc2V0IC1lXG4kU1VETyBta2RpciAtcCBcIiR7ZGlyZWN0b3J5fVwiXG4kU1VETyBjaG1vZCAke2RpcmVjdG9yeU1vZGV9IFwiJHtkaXJlY3Rvcnl9XCJcbiRTVURPIHRlZSBcIiR7ZmlsZVBhdGh9XCIgPiAvZGV2L251bGxcbiRTVURPIGNobW9kICR7ZmlsZU1vZGV9IFwiJHtmaWxlUGF0aH1cImAsXG4gICAgfSk7XG59XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZW1vdGVDb21tYW5kRXhpc3RzKGNvbmZpZzogeyBob3N0OiBzdHJpbmc7IGNvbW1hbmQ6IHN0cmluZyB9KSB7XG4gICAgbGV0IHsgaG9zdCwgY29tbWFuZCB9ID0gY29uZmlnO1xuICAgIGxldCByZXN1bHQgPSBhd2FpdCBydW5PdmVyU1NIKHtcbiAgICAgICAgaG9zdCxcbiAgICAgICAgc2NyaXB0OiBgY29tbWFuZCAtdiBcIiR7Y29tbWFuZH1cIiA+IC9kZXYvbnVsbCAyPiYxICYmIGVjaG8geWVzIHx8IGVjaG8gbm9gLFxuICAgICAgICBhbGxvd0ZhaWx1cmU6IHRydWUsXG4gICAgfSk7XG4gICAgcmV0dXJuIHJlc3VsdC5zdGRvdXQudHJpbSgpID09PSBcInllc1wiO1xufVxuIl19
|
|
90
|
+
/* _JS_SOURCE_HASH = "b9707fe65376e19327371edb2ebdda68daa9b9b21220d7ac91ec21ef22520290"; */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
|
+
//exports.spawnPromise = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
// PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js contains a plain JS port of spawnPromise, so it can run
|
|
6
|
+
// with no dependencies. If you change one, make the matching change in the other.
|
|
7
|
+
/** An argument list rather than a shell string, so hostnames and paths can never be parsed as
|
|
8
|
+
shell syntax. Resolves with the exit code instead of throwing, callers decide what a failure
|
|
9
|
+
means. `inheritStderr` lets a child's own errors reach the terminal as they happen. */
|
|
10
|
+
function spawnPromise(config) {
|
|
11
|
+
let { command, args, cwd, input, inheritStderr, timeoutTime } = config;
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
var _a, _b, _c, _d;
|
|
14
|
+
let child = (0, child_process_1.spawn)(command, args, {
|
|
15
|
+
cwd,
|
|
16
|
+
stdio: ["pipe", "pipe", inheritStderr && "inherit" || "pipe"],
|
|
17
|
+
});
|
|
18
|
+
let stdout = "";
|
|
19
|
+
let stderr = "";
|
|
20
|
+
// A command that never returns would otherwise hold up everything behind it.
|
|
21
|
+
let timer = timeoutTime && setTimeout(() => child.kill("SIGKILL"), timeoutTime) || undefined;
|
|
22
|
+
child.on("close", () => timer && clearTimeout(timer));
|
|
23
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on("data", chunk => stdout += chunk);
|
|
24
|
+
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on("data", chunk => stderr += chunk);
|
|
25
|
+
child.on("error", error => resolve({ stdout, stderr, status: undefined, error }));
|
|
26
|
+
child.on("close", status => resolve({ stdout, stderr, status: status !== null && status !== void 0 ? status : undefined, error: undefined }));
|
|
27
|
+
// A child that exits before reading everything would otherwise raise EPIPE.
|
|
28
|
+
(_c = child.stdin) === null || _c === void 0 ? void 0 : _c.on("error", () => undefined);
|
|
29
|
+
(_d = child.stdin) === null || _d === void 0 ? void 0 : _d.end(input || "");
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
exports.spawnPromise = spawnPromise;
|
|
33
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3Bhd24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJzcGF3bi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxpREFBc0M7QUFFdEMsMEhBQTBIO0FBQzFILGtGQUFrRjtBQUVsRjs7MEZBRTBGO0FBQzFGLFNBQWdCLFlBQVksQ0FBQyxNQU81QjtJQUNHLElBQUksRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsYUFBYSxFQUFFLFdBQVcsRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUN2RSxPQUFPLElBQUksT0FBTyxDQUEyRixPQUFPLENBQUMsRUFBRTs7UUFDbkgsSUFBSSxLQUFLLEdBQUcsSUFBQSxxQkFBSyxFQUFDLE9BQU8sRUFBRSxJQUFJLEVBQUU7WUFDN0IsR0FBRztZQUNILEtBQUssRUFBRSxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsYUFBYSxJQUFJLFNBQVMsSUFBSSxNQUFNLENBQUM7U0FDaEUsQ0FBQyxDQUFDO1FBQ0gsSUFBSSxNQUFNLEdBQUcsRUFBRSxDQUFDO1FBQ2hCLElBQUksTUFBTSxHQUFHLEVBQUUsQ0FBQztRQUNoQiw2RUFBNkU7UUFDN0UsSUFBSSxLQUFLLEdBQUcsV0FBVyxJQUFJLFVBQVUsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxJQUFJLFNBQVMsQ0FBQztRQUM3RixLQUFLLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxHQUFHLEVBQUUsQ0FBQyxLQUFLLElBQUksWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7UUFDdEQsTUFBQSxLQUFLLENBQUMsTUFBTSwwQ0FBRSxFQUFFLENBQUMsTUFBTSxFQUFFLEtBQUssQ0FBQyxFQUFFLENBQUMsTUFBTSxJQUFJLEtBQUssQ0FBQyxDQUFDO1FBQ25ELE1BQUEsS0FBSyxDQUFDLE1BQU0sMENBQUUsRUFBRSxDQUFDLE1BQU0sRUFBRSxLQUFLLENBQUMsRUFBRSxDQUFDLE1BQU0sSUFBSSxLQUFLLENBQUMsQ0FBQztRQUNuRCxLQUFLLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLFNBQVMsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUM7UUFDbEYsS0FBSyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDLEVBQUUsQ0FBQyxPQUFPLENBQUMsRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxNQUFNLGFBQU4sTUFBTSxjQUFOLE1BQU0sR0FBSSxTQUFTLEVBQUUsS0FBSyxFQUFFLFNBQVMsRUFBRSxDQUFDLENBQUMsQ0FBQztRQUN4Ryw0RUFBNEU7UUFDNUUsTUFBQSxLQUFLLENBQUMsS0FBSywwQ0FBRSxFQUFFLENBQUMsT0FBTyxFQUFFLEdBQUcsRUFBRSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQzFDLE1BQUEsS0FBSyxDQUFDLEtBQUssMENBQUUsR0FBRyxDQUFDLEtBQUssSUFBSSxFQUFFLENBQUMsQ0FBQztJQUNsQyxDQUFDLENBQUMsQ0FBQztBQUNQLENBQUM7QUEzQkQsb0NBMkJDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgc3Bhd24gfSBmcm9tIFwiY2hpbGRfcHJvY2Vzc1wiO1xuXG4vLyBQT1JURUQgQ09ERTogc2VjdXJpdHkvYXV0aG9yaXplZEtleXMvZGFlbW9uL3BvcnRzZWN1cmVEYWVtb24uanMgY29udGFpbnMgYSBwbGFpbiBKUyBwb3J0IG9mIHNwYXduUHJvbWlzZSwgc28gaXQgY2FuIHJ1blxuLy8gd2l0aCBubyBkZXBlbmRlbmNpZXMuIElmIHlvdSBjaGFuZ2Ugb25lLCBtYWtlIHRoZSBtYXRjaGluZyBjaGFuZ2UgaW4gdGhlIG90aGVyLlxuXG4vKiogQW4gYXJndW1lbnQgbGlzdCByYXRoZXIgdGhhbiBhIHNoZWxsIHN0cmluZywgc28gaG9zdG5hbWVzIGFuZCBwYXRocyBjYW4gbmV2ZXIgYmUgcGFyc2VkIGFzXG4gICAgc2hlbGwgc3ludGF4LiBSZXNvbHZlcyB3aXRoIHRoZSBleGl0IGNvZGUgaW5zdGVhZCBvZiB0aHJvd2luZywgY2FsbGVycyBkZWNpZGUgd2hhdCBhIGZhaWx1cmVcbiAgICBtZWFucy4gYGluaGVyaXRTdGRlcnJgIGxldHMgYSBjaGlsZCdzIG93biBlcnJvcnMgcmVhY2ggdGhlIHRlcm1pbmFsIGFzIHRoZXkgaGFwcGVuLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHNwYXduUHJvbWlzZShjb25maWc6IHtcbiAgICBjb21tYW5kOiBzdHJpbmc7XG4gICAgYXJnczogc3RyaW5nW107XG4gICAgY3dkPzogc3RyaW5nO1xuICAgIGlucHV0Pzogc3RyaW5nO1xuICAgIGluaGVyaXRTdGRlcnI/OiBib29sZWFuO1xuICAgIHRpbWVvdXRUaW1lPzogbnVtYmVyO1xufSkge1xuICAgIGxldCB7IGNvbW1hbmQsIGFyZ3MsIGN3ZCwgaW5wdXQsIGluaGVyaXRTdGRlcnIsIHRpbWVvdXRUaW1lIH0gPSBjb25maWc7XG4gICAgcmV0dXJuIG5ldyBQcm9taXNlPHsgc3Rkb3V0OiBzdHJpbmc7IHN0ZGVycjogc3RyaW5nOyBzdGF0dXM6IG51bWJlciB8IHVuZGVmaW5lZDsgZXJyb3I6IEVycm9yIHwgdW5kZWZpbmVkIH0+KHJlc29sdmUgPT4ge1xuICAgICAgICBsZXQgY2hpbGQgPSBzcGF3bihjb21tYW5kLCBhcmdzLCB7XG4gICAgICAgICAgICBjd2QsXG4gICAgICAgICAgICBzdGRpbzogW1wicGlwZVwiLCBcInBpcGVcIiwgaW5oZXJpdFN0ZGVyciAmJiBcImluaGVyaXRcIiB8fCBcInBpcGVcIl0sXG4gICAgICAgIH0pO1xuICAgICAgICBsZXQgc3Rkb3V0ID0gXCJcIjtcbiAgICAgICAgbGV0IHN0ZGVyciA9IFwiXCI7XG4gICAgICAgIC8vIEEgY29tbWFuZCB0aGF0IG5ldmVyIHJldHVybnMgd291bGQgb3RoZXJ3aXNlIGhvbGQgdXAgZXZlcnl0aGluZyBiZWhpbmQgaXQuXG4gICAgICAgIGxldCB0aW1lciA9IHRpbWVvdXRUaW1lICYmIHNldFRpbWVvdXQoKCkgPT4gY2hpbGQua2lsbChcIlNJR0tJTExcIiksIHRpbWVvdXRUaW1lKSB8fCB1bmRlZmluZWQ7XG4gICAgICAgIGNoaWxkLm9uKFwiY2xvc2VcIiwgKCkgPT4gdGltZXIgJiYgY2xlYXJUaW1lb3V0KHRpbWVyKSk7XG4gICAgICAgIGNoaWxkLnN0ZG91dD8ub24oXCJkYXRhXCIsIGNodW5rID0+IHN0ZG91dCArPSBjaHVuayk7XG4gICAgICAgIGNoaWxkLnN0ZGVycj8ub24oXCJkYXRhXCIsIGNodW5rID0+IHN0ZGVyciArPSBjaHVuayk7XG4gICAgICAgIGNoaWxkLm9uKFwiZXJyb3JcIiwgZXJyb3IgPT4gcmVzb2x2ZSh7IHN0ZG91dCwgc3RkZXJyLCBzdGF0dXM6IHVuZGVmaW5lZCwgZXJyb3IgfSkpO1xuICAgICAgICBjaGlsZC5vbihcImNsb3NlXCIsIHN0YXR1cyA9PiByZXNvbHZlKHsgc3Rkb3V0LCBzdGRlcnIsIHN0YXR1czogc3RhdHVzID8/IHVuZGVmaW5lZCwgZXJyb3I6IHVuZGVmaW5lZCB9KSk7XG4gICAgICAgIC8vIEEgY2hpbGQgdGhhdCBleGl0cyBiZWZvcmUgcmVhZGluZyBldmVyeXRoaW5nIHdvdWxkIG90aGVyd2lzZSByYWlzZSBFUElQRS5cbiAgICAgICAgY2hpbGQuc3RkaW4/Lm9uKFwiZXJyb3JcIiwgKCkgPT4gdW5kZWZpbmVkKTtcbiAgICAgICAgY2hpbGQuc3RkaW4/LmVuZChpbnB1dCB8fCBcIlwiKTtcbiAgICB9KTtcbn1cbiJdfQ==
|
|
34
|
+
/* _JS_SOURCE_HASH = "148887f3afb42a330df49eb955396403fab2e558c30741383b1a17b19a134288"; */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/** Windows shells do not expand ~ themselves, and hunting down your home folder by hand is
|
|
5
|
+
annoying, so we expand it on every platform. Both separators are accepted, because a Windows
|
|
6
|
+
user may type either one. */
|
|
7
|
+
export function expandHome(filePath: string) {
|
|
8
|
+
if (filePath === "~") {
|
|
9
|
+
return os.homedir();
|
|
10
|
+
}
|
|
11
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
12
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
13
|
+
}
|
|
14
|
+
if (filePath.startsWith("~")) {
|
|
15
|
+
// ~otheruser needs an account database we cannot read portably, and quietly resolving it
|
|
16
|
+
// to the wrong home would be worse than refusing.
|
|
17
|
+
throw new Error(`Expected ~ or an ordinary path, was ${filePath}. Referring to another user's home with ~name is not supported.`);
|
|
18
|
+
}
|
|
19
|
+
return path.resolve(filePath);
|
|
20
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawnPromise } from "./spawn";
|
|
2
|
+
|
|
3
|
+
const SSH_CONNECT_TIMEOUT = 10;
|
|
4
|
+
const EXISTS_MARKER = "__PORTSECURE_EXISTS__";
|
|
5
|
+
const MISSING_MARKER = "__PORTSECURE_MISSING__";
|
|
6
|
+
const MAX_ERROR_BODY_LENGTH = 500;
|
|
7
|
+
|
|
8
|
+
// /etc is root owned, so fall back to sudo whenever the SSH user is not root.
|
|
9
|
+
export const SUDO_PREAMBLE = `SUDO=""; if [ "$(id -u)" -ne 0 ]; then SUDO="sudo -n"; fi`;
|
|
10
|
+
|
|
11
|
+
/** The host string is handed to ssh untouched. Users, keys and ports belong in the caller's ssh
|
|
12
|
+
config, so BatchMode makes a missing setup fail immediately instead of prompting. */
|
|
13
|
+
export async function runOverSSH(config: { host: string; script: string; input?: string; allowFailure?: boolean }) {
|
|
14
|
+
let { host, script, input, allowFailure } = config;
|
|
15
|
+
let result = await spawnPromise({
|
|
16
|
+
command: "ssh",
|
|
17
|
+
args: [
|
|
18
|
+
"-o", "BatchMode=yes",
|
|
19
|
+
"-o", `ConnectTimeout=${SSH_CONNECT_TIMEOUT}`,
|
|
20
|
+
host,
|
|
21
|
+
script,
|
|
22
|
+
],
|
|
23
|
+
input,
|
|
24
|
+
inheritStderr: !allowFailure,
|
|
25
|
+
});
|
|
26
|
+
if (result.error) {
|
|
27
|
+
throw new Error(`Expected ssh to run against ${host}, failed with ${result.error.message}`);
|
|
28
|
+
}
|
|
29
|
+
if (result.status !== 0 && !allowFailure) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Expected ssh to ${host} to exit 0, was ${result.status}.`
|
|
32
|
+
+ ` Non-interactive ssh access to ${host} has to work on its own - fix it in your ssh config.\n`
|
|
33
|
+
+ `${(result.stdout + result.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return { stdout: result.stdout || "", stderr: result.stderr || "", status: result.status };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Returns undefined when the file does not exist, so a missing file reads differently from an
|
|
40
|
+
empty one. */
|
|
41
|
+
export async function readRemoteFile(config: { host: string; filePath: string }) {
|
|
42
|
+
let { host, filePath } = config;
|
|
43
|
+
let output = (await runOverSSH({
|
|
44
|
+
host,
|
|
45
|
+
script: `${SUDO_PREAMBLE}
|
|
46
|
+
if $SUDO test -f "${filePath}"; then
|
|
47
|
+
echo "${EXISTS_MARKER}"
|
|
48
|
+
$SUDO cat "${filePath}"
|
|
49
|
+
else
|
|
50
|
+
echo "${MISSING_MARKER}"
|
|
51
|
+
fi`,
|
|
52
|
+
})).stdout;
|
|
53
|
+
let newlineIndex = output.indexOf("\n");
|
|
54
|
+
let marker = output.slice(0, newlineIndex).trim();
|
|
55
|
+
if (marker === MISSING_MARKER) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (marker !== EXISTS_MARKER) {
|
|
59
|
+
throw new Error(`Expected ${EXISTS_MARKER} or ${MISSING_MARKER} from ${host}, was ${output.slice(0, MAX_ERROR_BODY_LENGTH)}`);
|
|
60
|
+
}
|
|
61
|
+
return output.slice(newlineIndex + 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function writeRemoteFile(config: {
|
|
65
|
+
host: string;
|
|
66
|
+
filePath: string;
|
|
67
|
+
contents: string;
|
|
68
|
+
fileMode: string;
|
|
69
|
+
directoryMode: string;
|
|
70
|
+
}) {
|
|
71
|
+
let { host, filePath, contents, fileMode, directoryMode } = config;
|
|
72
|
+
let directory = filePath.slice(0, filePath.lastIndexOf("/"));
|
|
73
|
+
await runOverSSH({
|
|
74
|
+
host,
|
|
75
|
+
// Deploying from Windows must not carry CRLF onto the target, where it breaks systemd
|
|
76
|
+
// units and makes private keys unreadable to ssh.
|
|
77
|
+
input: contents.replace(/\r\n/g, "\n"),
|
|
78
|
+
script: `${SUDO_PREAMBLE}
|
|
79
|
+
set -e
|
|
80
|
+
$SUDO mkdir -p "${directory}"
|
|
81
|
+
$SUDO chmod ${directoryMode} "${directory}"
|
|
82
|
+
$SUDO tee "${filePath}" > /dev/null
|
|
83
|
+
$SUDO chmod ${fileMode} "${filePath}"`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function remoteCommandExists(config: { host: string; command: string }) {
|
|
88
|
+
let { host, command } = config;
|
|
89
|
+
let result = await runOverSSH({
|
|
90
|
+
host,
|
|
91
|
+
script: `command -v "${command}" > /dev/null 2>&1 && echo yes || echo no`,
|
|
92
|
+
allowFailure: true,
|
|
93
|
+
});
|
|
94
|
+
return result.stdout.trim() === "yes";
|
|
95
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
|
|
3
|
+
// PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js contains a plain JS port of spawnPromise, so it can run
|
|
4
|
+
// with no dependencies. If you change one, make the matching change in the other.
|
|
5
|
+
|
|
6
|
+
/** An argument list rather than a shell string, so hostnames and paths can never be parsed as
|
|
7
|
+
shell syntax. Resolves with the exit code instead of throwing, callers decide what a failure
|
|
8
|
+
means. `inheritStderr` lets a child's own errors reach the terminal as they happen. */
|
|
9
|
+
export function spawnPromise(config: {
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
cwd?: string;
|
|
13
|
+
input?: string;
|
|
14
|
+
inheritStderr?: boolean;
|
|
15
|
+
timeoutTime?: number;
|
|
16
|
+
}) {
|
|
17
|
+
let { command, args, cwd, input, inheritStderr, timeoutTime } = config;
|
|
18
|
+
return new Promise<{ stdout: string; stderr: string; status: number | undefined; error: Error | undefined }>(resolve => {
|
|
19
|
+
let child = spawn(command, args, {
|
|
20
|
+
cwd,
|
|
21
|
+
stdio: ["pipe", "pipe", inheritStderr && "inherit" || "pipe"],
|
|
22
|
+
});
|
|
23
|
+
let stdout = "";
|
|
24
|
+
let stderr = "";
|
|
25
|
+
// A command that never returns would otherwise hold up everything behind it.
|
|
26
|
+
let timer = timeoutTime && setTimeout(() => child.kill("SIGKILL"), timeoutTime) || undefined;
|
|
27
|
+
child.on("close", () => timer && clearTimeout(timer));
|
|
28
|
+
child.stdout?.on("data", chunk => stdout += chunk);
|
|
29
|
+
child.stderr?.on("data", chunk => stderr += chunk);
|
|
30
|
+
child.on("error", error => resolve({ stdout, stderr, status: undefined, error }));
|
|
31
|
+
child.on("close", status => resolve({ stdout, stderr, status: status ?? undefined, error: undefined }));
|
|
32
|
+
// A child that exits before reading everything would otherwise raise EPIPE.
|
|
33
|
+
child.stdin?.on("error", () => undefined);
|
|
34
|
+
child.stdin?.end(input || "");
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import { expandHome } from "../helpers/paths";
|
|
4
|
+
import { formatOpenSSHPrivateKey, formatPublicKeyLine, parseOpenSSHPrivateKey, publicKeyFromSeed } from "./sshKeyFile";
|
|
5
|
+
|
|
6
|
+
const USAGE = `Usage: yarn derivekey <label> <source-key> <derived-key>
|
|
7
|
+
|
|
8
|
+
Derives a second ed25519 key from an existing one. The same label and source key always give the
|
|
9
|
+
same derived key, so it can be worked out again rather than backed up.
|
|
10
|
+
|
|
11
|
+
yarn derivekey revokegithubkey ~/authorized_keys_access/id_ed25519 ~/authorized_keys_access/id_ed25519_revoke`;
|
|
12
|
+
|
|
13
|
+
/** A label mixed into a key's secret to get another key, so one key can stand behind several
|
|
14
|
+
identities without any of them being stored.
|
|
15
|
+
|
|
16
|
+
HMAC rather than hashing the two joined together: with a plain hash, anyone who obtained one
|
|
17
|
+
derived key could extend it into further labels without ever knowing the source key. The label
|
|
18
|
+
is the message and the source secret is the key, which is what HMAC is for. */
|
|
19
|
+
export function deriveEd25519Key(config: { seed: Buffer; label: string }) {
|
|
20
|
+
let { seed, label } = config;
|
|
21
|
+
if (!label) {
|
|
22
|
+
throw new Error(`Expected a label to derive with, was ${JSON.stringify(label)}`);
|
|
23
|
+
}
|
|
24
|
+
let derivedSeed = crypto.createHmac("sha256", seed).update(label, "utf8").digest();
|
|
25
|
+
return { seed: derivedSeed, publicKey: publicKeyFromSeed(derivedSeed) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function pathExists(filePath: string) {
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(filePath);
|
|
31
|
+
return true;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv: string[]) {
|
|
38
|
+
if (argv.length !== 3) {
|
|
39
|
+
throw new Error(`Expected a label, a source key and a derived key, was ${argv.length} argument(s)\n${USAGE}`);
|
|
40
|
+
}
|
|
41
|
+
let [label, sourcePath, derivedPath] = argv;
|
|
42
|
+
return { label, sourcePath: expandHome(sourcePath), derivedPath: expandHome(derivedPath) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function main() {
|
|
46
|
+
let { label, sourcePath, derivedPath } = parseArgs(process.argv.slice(2));
|
|
47
|
+
|
|
48
|
+
if (!await pathExists(sourcePath)) {
|
|
49
|
+
throw new Error(`Expected a source key at ${sourcePath}, no such file exists`);
|
|
50
|
+
}
|
|
51
|
+
// Writing over a key would strand whatever it already gives access to, and the derived key can
|
|
52
|
+
// always be worked out again, so there is never a reason to overwrite.
|
|
53
|
+
if (await pathExists(derivedPath)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Expected ${derivedPath} to not exist, it does. Deriving the same label again gives the`
|
|
56
|
+
+ ` same key, so delete it first if you want it rewritten.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let source = parseOpenSSHPrivateKey(await fs.readFile(sourcePath, "utf8"));
|
|
61
|
+
let derived = deriveEd25519Key({ seed: source.seed, label });
|
|
62
|
+
let comment = `${source.comment || sourcePath} ${label}`.trim();
|
|
63
|
+
|
|
64
|
+
await fs.writeFile(derivedPath, formatOpenSSHPrivateKey({ seed: derived.seed, comment }), { mode: 0o600 });
|
|
65
|
+
let publicLine = formatPublicKeyLine({ publicKey: derived.publicKey, comment });
|
|
66
|
+
await fs.writeFile(`${derivedPath}.pub`, publicLine, { mode: 0o644 });
|
|
67
|
+
|
|
68
|
+
console.log(`Derived "${label}" from ${sourcePath}`);
|
|
69
|
+
console.log(` private ${derivedPath}`);
|
|
70
|
+
console.log(` public ${derivedPath}.pub`);
|
|
71
|
+
console.log(publicLine.trim());
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
6
|
+
//exports.main = exports.deriveEd25519Key = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const paths_1 = require("../helpers/paths");
|
|
10
|
+
const sshKeyFile_1 = require("./sshKeyFile");
|
|
11
|
+
const USAGE = `Usage: yarn derivekey <label> <source-key> <derived-key>
|
|
12
|
+
|
|
13
|
+
Derives a second ed25519 key from an existing one. The same label and source key always give the
|
|
14
|
+
same derived key, so it can be worked out again rather than backed up.
|
|
15
|
+
|
|
16
|
+
yarn derivekey revokegithubkey ~/authorized_keys_access/id_ed25519 ~/authorized_keys_access/id_ed25519_revoke`;
|
|
17
|
+
/** A label mixed into a key's secret to get another key, so one key can stand behind several
|
|
18
|
+
identities without any of them being stored.
|
|
19
|
+
|
|
20
|
+
HMAC rather than hashing the two joined together: with a plain hash, anyone who obtained one
|
|
21
|
+
derived key could extend it into further labels without ever knowing the source key. The label
|
|
22
|
+
is the message and the source secret is the key, which is what HMAC is for. */
|
|
23
|
+
function deriveEd25519Key(config) {
|
|
24
|
+
let { seed, label } = config;
|
|
25
|
+
if (!label) {
|
|
26
|
+
throw new Error(`Expected a label to derive with, was ${JSON.stringify(label)}`);
|
|
27
|
+
}
|
|
28
|
+
let derivedSeed = crypto_1.default.createHmac("sha256", seed).update(label, "utf8").digest();
|
|
29
|
+
return { seed: derivedSeed, publicKey: (0, sshKeyFile_1.publicKeyFromSeed)(derivedSeed) };
|
|
30
|
+
}
|
|
31
|
+
exports.deriveEd25519Key = deriveEd25519Key;
|
|
32
|
+
async function pathExists(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
await promises_1.default.access(filePath);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
if (argv.length !== 3) {
|
|
43
|
+
throw new Error(`Expected a label, a source key and a derived key, was ${argv.length} argument(s)\n${USAGE}`);
|
|
44
|
+
}
|
|
45
|
+
let [label, sourcePath, derivedPath] = argv;
|
|
46
|
+
return { label, sourcePath: (0, paths_1.expandHome)(sourcePath), derivedPath: (0, paths_1.expandHome)(derivedPath) };
|
|
47
|
+
}
|
|
48
|
+
async function main() {
|
|
49
|
+
let { label, sourcePath, derivedPath } = parseArgs(process.argv.slice(2));
|
|
50
|
+
if (!await pathExists(sourcePath)) {
|
|
51
|
+
throw new Error(`Expected a source key at ${sourcePath}, no such file exists`);
|
|
52
|
+
}
|
|
53
|
+
// Writing over a key would strand whatever it already gives access to, and the derived key can
|
|
54
|
+
// always be worked out again, so there is never a reason to overwrite.
|
|
55
|
+
if (await pathExists(derivedPath)) {
|
|
56
|
+
throw new Error(`Expected ${derivedPath} to not exist, it does. Deriving the same label again gives the`
|
|
57
|
+
+ ` same key, so delete it first if you want it rewritten.`);
|
|
58
|
+
}
|
|
59
|
+
let source = (0, sshKeyFile_1.parseOpenSSHPrivateKey)(await promises_1.default.readFile(sourcePath, "utf8"));
|
|
60
|
+
let derived = deriveEd25519Key({ seed: source.seed, label });
|
|
61
|
+
let comment = `${source.comment || sourcePath} ${label}`.trim();
|
|
62
|
+
await promises_1.default.writeFile(derivedPath, (0, sshKeyFile_1.formatOpenSSHPrivateKey)({ seed: derived.seed, comment }), { mode: 0o600 });
|
|
63
|
+
let publicLine = (0, sshKeyFile_1.formatPublicKeyLine)({ publicKey: derived.publicKey, comment });
|
|
64
|
+
await promises_1.default.writeFile(`${derivedPath}.pub`, publicLine, { mode: 0o644 });
|
|
65
|
+
console.log(`Derived "${label}" from ${sourcePath}`);
|
|
66
|
+
console.log(` private ${derivedPath}`);
|
|
67
|
+
console.log(` public ${derivedPath}.pub`);
|
|
68
|
+
console.log(publicLine.trim());
|
|
69
|
+
}
|
|
70
|
+
exports.main = main;
|
|
71
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVyaXZlS2V5LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGVyaXZlS2V5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQUFBLG9EQUE0QjtBQUM1QiwyREFBNkI7QUFDN0IsNENBQThDO0FBQzlDLDZDQUF1SDtBQUV2SCxNQUFNLEtBQUssR0FBRzs7Ozs7Z0hBS2tHLENBQUM7QUFFakg7Ozs7O2tGQUtrRjtBQUNsRixTQUFnQixnQkFBZ0IsQ0FBQyxNQUF1QztJQUNwRSxJQUFJLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUM3QixJQUFJLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDVCxNQUFNLElBQUksS0FBSyxDQUFDLHdDQUF3QyxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUNyRixDQUFDO0lBQ0QsSUFBSSxXQUFXLEdBQUcsZ0JBQU0sQ0FBQyxVQUFVLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUM7SUFDbkYsT0FBTyxFQUFFLElBQUksRUFBRSxXQUFXLEVBQUUsU0FBUyxFQUFFLElBQUEsOEJBQWlCLEVBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQztBQUM1RSxDQUFDO0FBUEQsNENBT0M7QUFFRCxLQUFLLFVBQVUsVUFBVSxDQUFDLFFBQWdCO0lBQ3RDLElBQUksQ0FBQztRQUNELE1BQU0sa0JBQUUsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDMUIsT0FBTyxJQUFJLENBQUM7SUFDaEIsQ0FBQztJQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7UUFDVCxPQUFPLEtBQUssQ0FBQztJQUNqQixDQUFDO0FBQ0wsQ0FBQztBQUVELFNBQVMsU0FBUyxDQUFDLElBQWM7SUFDN0IsSUFBSSxJQUFJLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3BCLE1BQU0sSUFBSSxLQUFLLENBQUMseURBQXlELElBQUksQ0FBQyxNQUFNLGlCQUFpQixLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBQ2xILENBQUM7SUFDRCxJQUFJLENBQUMsS0FBSyxFQUFFLFVBQVUsRUFBRSxXQUFXLENBQUMsR0FBRyxJQUFJLENBQUM7SUFDNUMsT0FBTyxFQUFFLEtBQUssRUFBRSxVQUFVLEVBQUUsSUFBQSxrQkFBVSxFQUFDLFVBQVUsQ0FBQyxFQUFFLFdBQVcsRUFBRSxJQUFBLGtCQUFVLEVBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQztBQUMvRixDQUFDO0FBRU0sS0FBSyxVQUFVLElBQUk7SUFDdEIsSUFBSSxFQUFFLEtBQUssRUFBRSxVQUFVLEVBQUUsV0FBVyxFQUFFLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFMUUsSUFBSSxDQUFDLE1BQU0sVUFBVSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7UUFDaEMsTUFBTSxJQUFJLEtBQUssQ0FBQyw0QkFBNEIsVUFBVSx1QkFBdUIsQ0FBQyxDQUFDO0lBQ25GLENBQUM7SUFDRCwrRkFBK0Y7SUFDL0YsdUVBQXVFO0lBQ3ZFLElBQUksTUFBTSxVQUFVLENBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQztRQUNoQyxNQUFNLElBQUksS0FBSyxDQUNYLFlBQVksV0FBVyxpRUFBaUU7Y0FDdEYseURBQXlELENBQzlELENBQUM7SUFDTixDQUFDO0lBRUQsSUFBSSxNQUFNLEdBQUcsSUFBQSxtQ0FBc0IsRUFBQyxNQUFNLGtCQUFFLENBQUMsUUFBUSxDQUFDLFVBQVUsRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDO0lBQzNFLElBQUksT0FBTyxHQUFHLGdCQUFnQixDQUFDLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztJQUM3RCxJQUFJLE9BQU8sR0FBRyxHQUFHLE1BQU0sQ0FBQyxPQUFPLElBQUksVUFBVSxJQUFJLEtBQUssRUFBRSxDQUFDLElBQUksRUFBRSxDQUFDO0lBRWhFLE1BQU0sa0JBQUUsQ0FBQyxTQUFTLENBQUMsV0FBVyxFQUFFLElBQUEsb0NBQXVCLEVBQUMsRUFBRSxJQUFJLEVBQUUsT0FBTyxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsQ0FBQyxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFDM0csSUFBSSxVQUFVLEdBQUcsSUFBQSxnQ0FBbUIsRUFBQyxFQUFFLFNBQVMsRUFBRSxPQUFPLENBQUMsU0FBUyxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUM7SUFDaEYsTUFBTSxrQkFBRSxDQUFDLFNBQVMsQ0FBQyxHQUFHLFdBQVcsTUFBTSxFQUFFLFVBQVUsRUFBRSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBRXRFLE9BQU8sQ0FBQyxHQUFHLENBQUMsWUFBWSxLQUFLLFVBQVUsVUFBVSxFQUFFLENBQUMsQ0FBQztJQUNyRCxPQUFPLENBQUMsR0FBRyxDQUFDLGFBQWEsV0FBVyxFQUFFLENBQUMsQ0FBQztJQUN4QyxPQUFPLENBQUMsR0FBRyxDQUFDLGFBQWEsV0FBVyxNQUFNLENBQUMsQ0FBQztJQUM1QyxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0FBQ25DLENBQUM7QUEzQkQsb0JBMkJDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGNyeXB0byBmcm9tIFwiY3J5cHRvXCI7XG5pbXBvcnQgZnMgZnJvbSBcImZzL3Byb21pc2VzXCI7XG5pbXBvcnQgeyBleHBhbmRIb21lIH0gZnJvbSBcIi4uL2hlbHBlcnMvcGF0aHNcIjtcbmltcG9ydCB7IGZvcm1hdE9wZW5TU0hQcml2YXRlS2V5LCBmb3JtYXRQdWJsaWNLZXlMaW5lLCBwYXJzZU9wZW5TU0hQcml2YXRlS2V5LCBwdWJsaWNLZXlGcm9tU2VlZCB9IGZyb20gXCIuL3NzaEtleUZpbGVcIjtcblxuY29uc3QgVVNBR0UgPSBgVXNhZ2U6IHlhcm4gZGVyaXZla2V5IDxsYWJlbD4gPHNvdXJjZS1rZXk+IDxkZXJpdmVkLWtleT5cblxuRGVyaXZlcyBhIHNlY29uZCBlZDI1NTE5IGtleSBmcm9tIGFuIGV4aXN0aW5nIG9uZS4gVGhlIHNhbWUgbGFiZWwgYW5kIHNvdXJjZSBrZXkgYWx3YXlzIGdpdmUgdGhlXG5zYW1lIGRlcml2ZWQga2V5LCBzbyBpdCBjYW4gYmUgd29ya2VkIG91dCBhZ2FpbiByYXRoZXIgdGhhbiBiYWNrZWQgdXAuXG5cbiAgeWFybiBkZXJpdmVrZXkgcmV2b2tlZ2l0aHVia2V5IH4vYXV0aG9yaXplZF9rZXlzX2FjY2Vzcy9pZF9lZDI1NTE5IH4vYXV0aG9yaXplZF9rZXlzX2FjY2Vzcy9pZF9lZDI1NTE5X3Jldm9rZWA7XG5cbi8qKiBBIGxhYmVsIG1peGVkIGludG8gYSBrZXkncyBzZWNyZXQgdG8gZ2V0IGFub3RoZXIga2V5LCBzbyBvbmUga2V5IGNhbiBzdGFuZCBiZWhpbmQgc2V2ZXJhbFxuICAgIGlkZW50aXRpZXMgd2l0aG91dCBhbnkgb2YgdGhlbSBiZWluZyBzdG9yZWQuXG5cbiAgICBITUFDIHJhdGhlciB0aGFuIGhhc2hpbmcgdGhlIHR3byBqb2luZWQgdG9nZXRoZXI6IHdpdGggYSBwbGFpbiBoYXNoLCBhbnlvbmUgd2hvIG9idGFpbmVkIG9uZVxuICAgIGRlcml2ZWQga2V5IGNvdWxkIGV4dGVuZCBpdCBpbnRvIGZ1cnRoZXIgbGFiZWxzIHdpdGhvdXQgZXZlciBrbm93aW5nIHRoZSBzb3VyY2Uga2V5LiBUaGUgbGFiZWxcbiAgICBpcyB0aGUgbWVzc2FnZSBhbmQgdGhlIHNvdXJjZSBzZWNyZXQgaXMgdGhlIGtleSwgd2hpY2ggaXMgd2hhdCBITUFDIGlzIGZvci4gKi9cbmV4cG9ydCBmdW5jdGlvbiBkZXJpdmVFZDI1NTE5S2V5KGNvbmZpZzogeyBzZWVkOiBCdWZmZXI7IGxhYmVsOiBzdHJpbmcgfSkge1xuICAgIGxldCB7IHNlZWQsIGxhYmVsIH0gPSBjb25maWc7XG4gICAgaWYgKCFsYWJlbCkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGEgbGFiZWwgdG8gZGVyaXZlIHdpdGgsIHdhcyAke0pTT04uc3RyaW5naWZ5KGxhYmVsKX1gKTtcbiAgICB9XG4gICAgbGV0IGRlcml2ZWRTZWVkID0gY3J5cHRvLmNyZWF0ZUhtYWMoXCJzaGEyNTZcIiwgc2VlZCkudXBkYXRlKGxhYmVsLCBcInV0ZjhcIikuZGlnZXN0KCk7XG4gICAgcmV0dXJuIHsgc2VlZDogZGVyaXZlZFNlZWQsIHB1YmxpY0tleTogcHVibGljS2V5RnJvbVNlZWQoZGVyaXZlZFNlZWQpIH07XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHBhdGhFeGlzdHMoZmlsZVBhdGg6IHN0cmluZykge1xuICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IGZzLmFjY2VzcyhmaWxlUGF0aCk7XG4gICAgICAgIHJldHVybiB0cnVlO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbn1cblxuZnVuY3Rpb24gcGFyc2VBcmdzKGFyZ3Y6IHN0cmluZ1tdKSB7XG4gICAgaWYgKGFyZ3YubGVuZ3RoICE9PSAzKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgYSBsYWJlbCwgYSBzb3VyY2Uga2V5IGFuZCBhIGRlcml2ZWQga2V5LCB3YXMgJHthcmd2Lmxlbmd0aH0gYXJndW1lbnQocylcXG4ke1VTQUdFfWApO1xuICAgIH1cbiAgICBsZXQgW2xhYmVsLCBzb3VyY2VQYXRoLCBkZXJpdmVkUGF0aF0gPSBhcmd2O1xuICAgIHJldHVybiB7IGxhYmVsLCBzb3VyY2VQYXRoOiBleHBhbmRIb21lKHNvdXJjZVBhdGgpLCBkZXJpdmVkUGF0aDogZXhwYW5kSG9tZShkZXJpdmVkUGF0aCkgfTtcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIG1haW4oKSB7XG4gICAgbGV0IHsgbGFiZWwsIHNvdXJjZVBhdGgsIGRlcml2ZWRQYXRoIH0gPSBwYXJzZUFyZ3MocHJvY2Vzcy5hcmd2LnNsaWNlKDIpKTtcblxuICAgIGlmICghYXdhaXQgcGF0aEV4aXN0cyhzb3VyY2VQYXRoKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGEgc291cmNlIGtleSBhdCAke3NvdXJjZVBhdGh9LCBubyBzdWNoIGZpbGUgZXhpc3RzYCk7XG4gICAgfVxuICAgIC8vIFdyaXRpbmcgb3ZlciBhIGtleSB3b3VsZCBzdHJhbmQgd2hhdGV2ZXIgaXQgYWxyZWFkeSBnaXZlcyBhY2Nlc3MgdG8sIGFuZCB0aGUgZGVyaXZlZCBrZXkgY2FuXG4gICAgLy8gYWx3YXlzIGJlIHdvcmtlZCBvdXQgYWdhaW4sIHNvIHRoZXJlIGlzIG5ldmVyIGEgcmVhc29uIHRvIG92ZXJ3cml0ZS5cbiAgICBpZiAoYXdhaXQgcGF0aEV4aXN0cyhkZXJpdmVkUGF0aCkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYEV4cGVjdGVkICR7ZGVyaXZlZFBhdGh9IHRvIG5vdCBleGlzdCwgaXQgZG9lcy4gRGVyaXZpbmcgdGhlIHNhbWUgbGFiZWwgYWdhaW4gZ2l2ZXMgdGhlYFxuICAgICAgICAgICAgKyBgIHNhbWUga2V5LCBzbyBkZWxldGUgaXQgZmlyc3QgaWYgeW91IHdhbnQgaXQgcmV3cml0dGVuLmBcbiAgICAgICAgKTtcbiAgICB9XG5cbiAgICBsZXQgc291cmNlID0gcGFyc2VPcGVuU1NIUHJpdmF0ZUtleShhd2FpdCBmcy5yZWFkRmlsZShzb3VyY2VQYXRoLCBcInV0ZjhcIikpO1xuICAgIGxldCBkZXJpdmVkID0gZGVyaXZlRWQyNTUxOUtleSh7IHNlZWQ6IHNvdXJjZS5zZWVkLCBsYWJlbCB9KTtcbiAgICBsZXQgY29tbWVudCA9IGAke3NvdXJjZS5jb21tZW50IHx8IHNvdXJjZVBhdGh9ICR7bGFiZWx9YC50cmltKCk7XG5cbiAgICBhd2FpdCBmcy53cml0ZUZpbGUoZGVyaXZlZFBhdGgsIGZvcm1hdE9wZW5TU0hQcml2YXRlS2V5KHsgc2VlZDogZGVyaXZlZC5zZWVkLCBjb21tZW50IH0pLCB7IG1vZGU6IDBvNjAwIH0pO1xuICAgIGxldCBwdWJsaWNMaW5lID0gZm9ybWF0UHVibGljS2V5TGluZSh7IHB1YmxpY0tleTogZGVyaXZlZC5wdWJsaWNLZXksIGNvbW1lbnQgfSk7XG4gICAgYXdhaXQgZnMud3JpdGVGaWxlKGAke2Rlcml2ZWRQYXRofS5wdWJgLCBwdWJsaWNMaW5lLCB7IG1vZGU6IDBvNjQ0IH0pO1xuXG4gICAgY29uc29sZS5sb2coYERlcml2ZWQgXCIke2xhYmVsfVwiIGZyb20gJHtzb3VyY2VQYXRofWApO1xuICAgIGNvbnNvbGUubG9nKGAgIHByaXZhdGUgJHtkZXJpdmVkUGF0aH1gKTtcbiAgICBjb25zb2xlLmxvZyhgICBwdWJsaWMgICR7ZGVyaXZlZFBhdGh9LnB1YmApO1xuICAgIGNvbnNvbGUubG9nKHB1YmxpY0xpbmUudHJpbSgpKTtcbn1cbiJdfQ==
|
|
72
|
+
/* _JS_SOURCE_HASH = "7d3b190d59d20686acd4f001d6c457d0b90d75d2ae382d6aff4571bb3310be76"; */
|