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,291 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { spawnPromise } from "../../helpers/spawn";
|
|
5
|
+
import { normalizeKeys } from "../authorizedKeys";
|
|
6
|
+
import { sourceRepoPath } from "../sources";
|
|
7
|
+
import { MANIFEST_NAME, normalizeContent, SIGN_NAMESPACE, SIGNATURE_NAME } from "../../signedFiles/manifest";
|
|
8
|
+
import { MAX_ERROR_BODY_LENGTH, SIGNER_CHANGE_DELAY } from "./paths";
|
|
9
|
+
import { notify } from "./notify";
|
|
10
|
+
import { saveState, sourceState } from "./state";
|
|
11
|
+
|
|
12
|
+
// A source that has never been signed reads as this, so losing a signature counts as a change of
|
|
13
|
+
// signer rather than as something to wave through.
|
|
14
|
+
const UNSIGNED = "";
|
|
15
|
+
// What fixes both of the signature problems we report, so the message can say so rather than
|
|
16
|
+
// leaving someone to work it out.
|
|
17
|
+
const SIGN_COMMAND = "yarn signfiles git";
|
|
18
|
+
|
|
19
|
+
async function pathExists(filePath: string) {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(filePath);
|
|
22
|
+
return true;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readSSHString(buffer: Buffer, offset: number) {
|
|
29
|
+
let length = buffer.readUInt32BE(offset);
|
|
30
|
+
return buffer.subarray(offset + 4, offset + 4 + length);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The signature carries the public key that made it, so the key itself can be reported rather
|
|
34
|
+
than only a fingerprint. Returns it in the usual "type base64" form. */
|
|
35
|
+
export function publicKeyFromSignature(signatureText: string) {
|
|
36
|
+
let body = signatureText.split("\n").filter(line => line && !line.startsWith("-----")).join("");
|
|
37
|
+
let blob = Buffer.from(body, "base64");
|
|
38
|
+
if (blob.subarray(0, 6).toString() !== "SSHSIG") {
|
|
39
|
+
throw new Error(`Expected an SSHSIG signature, started with ${blob.subarray(0, 6).toString()}`);
|
|
40
|
+
}
|
|
41
|
+
// The magic, then a uint32 version, then the public key.
|
|
42
|
+
let publicKey = readSSHString(blob, 6 + 4);
|
|
43
|
+
let keyType = readSSHString(publicKey, 0).toString();
|
|
44
|
+
let fingerprint = "SHA256:" + crypto.createHash("sha256").update(publicKey).digest("base64").replace(/=+$/, "");
|
|
45
|
+
return { publicKey: `${keyType} ${publicKey.toString("base64")}`, fingerprint };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Reads one checkout's keys. Prefers a top level authorized_keys file and otherwise concatenates
|
|
49
|
+
every .pub at the top level. */
|
|
50
|
+
export async function readCheckoutKeys(repoPath: string) {
|
|
51
|
+
let combinedPath = path.join(repoPath, "authorized_keys");
|
|
52
|
+
if (await pathExists(combinedPath)) {
|
|
53
|
+
return normalizeKeys(await fs.readFile(combinedPath, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
let pubFiles = (await fs.readdir(repoPath)).filter(name => name.endsWith(".pub")).sort();
|
|
56
|
+
if (!pubFiles.length) {
|
|
57
|
+
throw new Error(`Expected authorized_keys or at least one .pub file in ${repoPath}, found neither`);
|
|
58
|
+
}
|
|
59
|
+
let keys: string[] = [];
|
|
60
|
+
for (let name of pubFiles) {
|
|
61
|
+
keys.push(...normalizeKeys(await fs.readFile(path.join(repoPath, name), "utf8")));
|
|
62
|
+
}
|
|
63
|
+
return keys;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Every file in a checkout, other than git's own directory and the signature files, which cannot
|
|
67
|
+
describe themselves. */
|
|
68
|
+
async function listCheckoutFiles(repoPath: string, prefix?: string): Promise<string[]> {
|
|
69
|
+
let files: string[] = [];
|
|
70
|
+
for (let entry of await fs.readdir(path.join(repoPath, prefix || ""), { withFileTypes: true })) {
|
|
71
|
+
let relativePath = prefix && `${prefix}/${entry.name}` || entry.name;
|
|
72
|
+
if (entry.name === ".git") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!prefix && (entry.name === MANIFEST_NAME || entry.name === SIGNATURE_NAME)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
files.push(...await listCheckoutFiles(repoPath, relativePath));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
files.push(relativePath);
|
|
83
|
+
}
|
|
84
|
+
return files.sort();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The signature only covers the manifest, so the manifest has to be checked against what is
|
|
88
|
+
actually on disk. Both directions matter: a missing file changes what the keys mean, and an
|
|
89
|
+
extra unlisted file could add keys nobody signed for. */
|
|
90
|
+
async function verifyManifestMatchesFiles(repoPath: string) {
|
|
91
|
+
let manifest = JSON.parse(await fs.readFile(path.join(repoPath, MANIFEST_NAME), "utf8"));
|
|
92
|
+
let listed = new Map<string, { path: string; size: number; sha256: string }>(
|
|
93
|
+
(manifest.files || []).map((file: { path: string }) => [file.path, file])
|
|
94
|
+
);
|
|
95
|
+
let actual = await listCheckoutFiles(repoPath);
|
|
96
|
+
|
|
97
|
+
let missing = [...listed.keys()].filter(filePath => !actual.includes(filePath));
|
|
98
|
+
if (missing.length) {
|
|
99
|
+
throw new Error(`Expected the signed files to be present, ${missing.length} missing, first ${missing[0]}`);
|
|
100
|
+
}
|
|
101
|
+
let extra = actual.filter(filePath => !listed.has(filePath));
|
|
102
|
+
if (extra.length) {
|
|
103
|
+
throw new Error(`Expected only signed files to be present, ${extra.length} extra, first ${extra[0]}`);
|
|
104
|
+
}
|
|
105
|
+
for (let filePath of actual) {
|
|
106
|
+
let expected = listed.get(filePath);
|
|
107
|
+
if (!expected) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
let contents = normalizeContent(await fs.readFile(path.join(repoPath, filePath)));
|
|
111
|
+
if (contents.length !== expected.size) {
|
|
112
|
+
throw new Error(`Expected ${filePath} to be ${expected.size} bytes, was ${contents.length}`);
|
|
113
|
+
}
|
|
114
|
+
let hash = crypto.createHash("sha256").update(contents).digest("hex");
|
|
115
|
+
if (hash !== expected.sha256) {
|
|
116
|
+
throw new Error(`Expected ${filePath} to hash to ${expected.sha256}, was ${hash}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The manifest and signature as they stand, whether or not they are any good. The hashes are
|
|
122
|
+
what tell a signature that was never updated apart from one that is broken. */
|
|
123
|
+
async function readSignatureFiles(repoPath: string) {
|
|
124
|
+
let manifestPath = path.join(repoPath, MANIFEST_NAME);
|
|
125
|
+
let signaturePath = path.join(repoPath, SIGNATURE_NAME);
|
|
126
|
+
let manifest = await pathExists(manifestPath) && await fs.readFile(manifestPath) || undefined;
|
|
127
|
+
let signature = await pathExists(signaturePath) && await fs.readFile(signaturePath) || undefined;
|
|
128
|
+
return {
|
|
129
|
+
manifest,
|
|
130
|
+
signature,
|
|
131
|
+
manifestHash: manifest && crypto.createHash("sha256").update(manifest).digest("hex") || "",
|
|
132
|
+
signatureHash: signature && crypto.createHash("sha256").update(signature).digest("hex") || "",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Who signed this checkout. Returns UNSIGNED when there is no signature at all, and throws when
|
|
137
|
+
there is one that does not hold up - an unverifiable signature is never treated as an identity,
|
|
138
|
+
so it can never become something we accept. */
|
|
139
|
+
async function verifyCheckoutSigner(config: {
|
|
140
|
+
repoPath: string;
|
|
141
|
+
files: { manifest?: Buffer; signature?: Buffer };
|
|
142
|
+
}) {
|
|
143
|
+
let { repoPath, files } = config;
|
|
144
|
+
if (!files.manifest && !files.signature) {
|
|
145
|
+
return UNSIGNED;
|
|
146
|
+
}
|
|
147
|
+
if (!files.manifest || !files.signature) {
|
|
148
|
+
throw new Error(`Expected both ${MANIFEST_NAME} and ${SIGNATURE_NAME}, only one is present`);
|
|
149
|
+
}
|
|
150
|
+
let result = await spawnPromise({
|
|
151
|
+
command: "ssh-keygen",
|
|
152
|
+
args: ["-Y", "check-novalidate", "-n", SIGN_NAMESPACE, "-s", path.join(repoPath, SIGNATURE_NAME)],
|
|
153
|
+
// The signature covers the normalised bytes, so a checkout that arrived with CRLF still
|
|
154
|
+
// verifies rather than looking like tampering.
|
|
155
|
+
input: normalizeContent(files.manifest).toString("utf8"),
|
|
156
|
+
});
|
|
157
|
+
if (result.status !== 0) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`the signature over ${MANIFEST_NAME} does not verify: `
|
|
160
|
+
+ `${(result.stdout + result.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
let reported = `${result.stdout} ${result.stderr}`.match(/(SHA256:[A-Za-z0-9+/=]+)/);
|
|
164
|
+
if (!reported) {
|
|
165
|
+
throw new Error(`ssh-keygen reported no signer fingerprint, said ${result.stdout.slice(0, MAX_ERROR_BODY_LENGTH)}`);
|
|
166
|
+
}
|
|
167
|
+
let { publicKey, fingerprint } = publicKeyFromSignature(files.signature.toString("utf8"));
|
|
168
|
+
// The key we read out of the signature has to be the one ssh-keygen just checked against,
|
|
169
|
+
// otherwise we would be reporting an identity that did not sign anything.
|
|
170
|
+
if (fingerprint !== reported[1]) {
|
|
171
|
+
throw new Error(`the signing key reads as ${fingerprint} but ssh-keygen verified ${reported[1]}`);
|
|
172
|
+
}
|
|
173
|
+
await verifyManifestMatchesFiles(repoPath);
|
|
174
|
+
return publicKey;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function describeSigner(signer: string) {
|
|
178
|
+
return signer === UNSIGNED && "<no public key>" || signer;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Reports a problem with a source's signature, but only when it is not the same problem we
|
|
182
|
+
already reported, so a fault that persists does not repeat every check. */
|
|
183
|
+
async function reportProblem(config: { repoURL: string; problem: string; message: string }) {
|
|
184
|
+
let { repoURL, problem, message } = config;
|
|
185
|
+
let sourceStateValue = sourceState(repoURL);
|
|
186
|
+
if (sourceStateValue.reportedProblem === problem) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
sourceStateValue.reportedProblem = problem;
|
|
190
|
+
await saveState();
|
|
191
|
+
await notify(message);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The keys a source is allowed to contribute right now. A source signed by someone we have not
|
|
195
|
+
accepted keeps contributing the keys we last accepted, until the delay has passed. */
|
|
196
|
+
export async function resolveSourceKeys(repoURL: string) {
|
|
197
|
+
let sourceStateValue = sourceState(repoURL);
|
|
198
|
+
let repoPath = sourceRepoPath(repoURL);
|
|
199
|
+
let files = await readSignatureFiles(repoPath);
|
|
200
|
+
|
|
201
|
+
let signer: string;
|
|
202
|
+
try {
|
|
203
|
+
signer = await verifyCheckoutSigner({ repoPath, files });
|
|
204
|
+
} catch (e) {
|
|
205
|
+
// Nothing here is trustworthy, so nothing here is used. Which of the two problems it is
|
|
206
|
+
// depends on whether the signature is simply the one we already accepted.
|
|
207
|
+
let unchanged = files.manifestHash === sourceStateValue.acceptedManifestHash
|
|
208
|
+
&& files.signatureHash === sourceStateValue.acceptedSignatureHash;
|
|
209
|
+
if (unchanged) {
|
|
210
|
+
await reportProblem({
|
|
211
|
+
repoURL,
|
|
212
|
+
problem: "stale",
|
|
213
|
+
message: `\`${repoURL}\` changed but its signature was not updated, so the changes are being`
|
|
214
|
+
+ ` ignored. Still using the keys signed by \`${describeSigner(sourceStateValue.acceptedSigner)}\`.`
|
|
215
|
+
+ `\nTo deploy the change, run this in that repo:\n\`\`\`\n${SIGN_COMMAND}\n\`\`\``,
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
await reportProblem({
|
|
219
|
+
repoURL,
|
|
220
|
+
problem: "corrupt",
|
|
221
|
+
message: `\`${repoURL}\` has a corrupted signature, so its contents are being ignored:`
|
|
222
|
+
+ ` ${e}.\nStill using the keys signed by \`${describeSigner(sourceStateValue.acceptedSigner)}\`.`
|
|
223
|
+
+ `\nTo replace it, run this in that repo:\n\`\`\`\n${SIGN_COMMAND}\n\`\`\``,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return sourceStateValue.acceptedKeys;
|
|
227
|
+
}
|
|
228
|
+
if (sourceStateValue.reportedProblem) {
|
|
229
|
+
sourceStateValue.reportedProblem = "";
|
|
230
|
+
await saveState();
|
|
231
|
+
}
|
|
232
|
+
let checkoutKeys = await readCheckoutKeys(repoPath);
|
|
233
|
+
|
|
234
|
+
let accept = async () => {
|
|
235
|
+
sourceStateValue.accepted = true;
|
|
236
|
+
sourceStateValue.acceptedSigner = signer;
|
|
237
|
+
sourceStateValue.acceptedKeys = checkoutKeys;
|
|
238
|
+
sourceStateValue.acceptedManifestHash = files.manifestHash;
|
|
239
|
+
sourceStateValue.acceptedSignatureHash = files.signatureHash;
|
|
240
|
+
sourceStateValue.pendingSigner = UNSIGNED;
|
|
241
|
+
sourceStateValue.pendingSince = 0;
|
|
242
|
+
await saveState();
|
|
243
|
+
return checkoutKeys;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Nothing has ever been accepted from this source, so this is what we start trusting.
|
|
247
|
+
if (!sourceStateValue.accepted) {
|
|
248
|
+
console.log(`Trusting ${repoURL} as signed by ${describeSigner(signer)}`);
|
|
249
|
+
return await accept();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (signer === sourceStateValue.acceptedSigner) {
|
|
253
|
+
// Back to the signer we already trust, so anything we were waiting on is moot.
|
|
254
|
+
if (sourceStateValue.pendingSince) {
|
|
255
|
+
console.log(`${repoURL} is signed by its accepted key again, dropping the pending change`);
|
|
256
|
+
}
|
|
257
|
+
return await accept();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Going from nothing to something signed is only ever an improvement, so it does not wait.
|
|
261
|
+
if (sourceStateValue.acceptedSigner === UNSIGNED) {
|
|
262
|
+
await notify(
|
|
263
|
+
`\`${repoURL}\` is now signed, by \`${signer}\`. It was not signed before, so its keys are`
|
|
264
|
+
+ ` being applied right away.`
|
|
265
|
+
);
|
|
266
|
+
return await accept();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// A signer we have not accepted. Anything new restarts the wait, so publishing twice in a row
|
|
270
|
+
// gains an attacker nothing. pendingSince is what marks a wait as running, because an unsigned
|
|
271
|
+
// checkout is itself a signer value and cannot double as "nothing pending".
|
|
272
|
+
if (!sourceStateValue.pendingSince || signer !== sourceStateValue.pendingSigner) {
|
|
273
|
+
sourceStateValue.pendingSigner = signer;
|
|
274
|
+
sourceStateValue.pendingSince = Date.now();
|
|
275
|
+
await saveState();
|
|
276
|
+
await notify(
|
|
277
|
+
`\`${repoURL}\` is now signed by \`${describeSigner(signer)}\`, where it was signed by`
|
|
278
|
+
+ ` \`${describeSigner(sourceStateValue.acceptedSigner)}\`. Its keys are NOT being applied.`
|
|
279
|
+
+ ` If nothing changes they will be applied in 24 hours.`
|
|
280
|
+
);
|
|
281
|
+
return sourceStateValue.acceptedKeys;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (Date.now() - sourceStateValue.pendingSince < SIGNER_CHANGE_DELAY) {
|
|
285
|
+
return sourceStateValue.acceptedKeys;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Same new signer, 24 hours later, and nobody stopped it.
|
|
289
|
+
console.log(`Accepting ${describeSigner(signer)} for ${repoURL} after the ${SIGNER_CHANGE_DELAY}ms wait`);
|
|
290
|
+
return await accept();
|
|
291
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { summarizeKey } from "../authorizedKeys";
|
|
5
|
+
import { PASSWD_PATH, ROOT_AUTHORIZED_KEYS } from "./paths";
|
|
6
|
+
import { notify } from "./notify";
|
|
7
|
+
import { getState, saveState } from "./state";
|
|
8
|
+
import { readAuthorizedKeysFile } from "./rootKeys";
|
|
9
|
+
|
|
10
|
+
async function pathExists(filePath: string) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(filePath);
|
|
13
|
+
return true;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function listUserAuthorizedKeyFiles() {
|
|
20
|
+
let entries: { name: string; filePath: string }[] = [];
|
|
21
|
+
let passwd = await fs.readFile(PASSWD_PATH, "utf8");
|
|
22
|
+
for (let line of passwd.split("\n")) {
|
|
23
|
+
let fields = line.split(":");
|
|
24
|
+
if (fields.length < 7) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
let [name, , , , , home] = fields;
|
|
28
|
+
if (!home || !await pathExists(home)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let filePath = path.join(home, ".ssh", "authorized_keys");
|
|
32
|
+
if (filePath === ROOT_AUTHORIZED_KEYS) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
entries.push({ name, filePath });
|
|
36
|
+
}
|
|
37
|
+
return entries;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hashKeys(keys: string[]) {
|
|
41
|
+
return crypto.createHash("sha256").update(keys.join("\n")).digest("hex");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Seeds the per user hashes without reporting every existing file as a change. */
|
|
45
|
+
export async function seedUserKeys() {
|
|
46
|
+
let state = getState();
|
|
47
|
+
if (Object.keys(state.userKeyHashes).length) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (let entry of await listUserAuthorizedKeyFiles()) {
|
|
51
|
+
state.userKeyHashes[entry.name] = hashKeys(await readAuthorizedKeysFile(entry.filePath));
|
|
52
|
+
}
|
|
53
|
+
await saveState();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Root is enforced elsewhere, every other account is watched and reported on. */
|
|
57
|
+
export async function checkOtherUserKeys() {
|
|
58
|
+
let state = getState();
|
|
59
|
+
let hashes: { [name: string]: string } = {};
|
|
60
|
+
for (let entry of await listUserAuthorizedKeyFiles()) {
|
|
61
|
+
let keys = await readAuthorizedKeysFile(entry.filePath);
|
|
62
|
+
let hash = hashKeys(keys);
|
|
63
|
+
hashes[entry.name] = hash;
|
|
64
|
+
let previousHash = state.userKeyHashes[entry.name];
|
|
65
|
+
if (previousHash === undefined || previousHash === hash) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
await notify(
|
|
69
|
+
`authorized_keys for user \`${entry.name}\` changed (\`${entry.filePath}\`).`
|
|
70
|
+
+ ` portsecure does not manage this account, so the change was left in place.`
|
|
71
|
+
+ `\n\`\`\`\n${keys.map(summarizeKey).join("\n") || "(now empty)"}\n\`\`\``
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
state.userKeyHashes = hashes;
|
|
75
|
+
await saveState();
|
|
76
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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.readRepoKeys = exports.summarizeKey = exports.keyRestriction = exports.keyFingerprint = exports.normalizeKeys = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
// PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js contains a plain JS port of normalizeKeys,
|
|
11
|
+
// summarizeKey and readRepoKeys, so it can resolve the same keys with no dependencies. The two
|
|
12
|
+
// must agree on which keys a repo produces - if you change one, make the matching change in the
|
|
13
|
+
// other.
|
|
14
|
+
function normalizeKeys(contents) {
|
|
15
|
+
return contents.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("#"));
|
|
16
|
+
}
|
|
17
|
+
exports.normalizeKeys = normalizeKeys;
|
|
18
|
+
/** The fingerprint ssh itself reports for a key, which is what the sshd log names and therefore
|
|
19
|
+
what a revocation is keyed by. Returns "" for a line that holds no key. */
|
|
20
|
+
function keyFingerprint(keyLine) {
|
|
21
|
+
let parts = keyLine.trim().split(/\s+/);
|
|
22
|
+
let typeIndex = parts.findIndex(part => /^(ssh-|ecdsa-|sk-)/.test(part));
|
|
23
|
+
let blob = typeIndex >= 0 && parts[typeIndex + 1] || "";
|
|
24
|
+
if (!blob) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
return "SHA256:" + crypto_1.default.createHash("sha256").update(Buffer.from(blob, "base64")).digest("base64").replace(/=+$/, "");
|
|
28
|
+
}
|
|
29
|
+
exports.keyFingerprint = keyFingerprint;
|
|
30
|
+
/** The addresses a key may be used from, which is the part of an authorized_keys line that
|
|
31
|
+
decides how much a stolen key is worth. A key with no restriction says so loudly. */
|
|
32
|
+
function keyRestriction(keyLine) {
|
|
33
|
+
let match = keyLine.match(/from="([^"]*)"/);
|
|
34
|
+
if (!match) {
|
|
35
|
+
return "ANY ADDRESS (no from= restriction)";
|
|
36
|
+
}
|
|
37
|
+
return match[1];
|
|
38
|
+
}
|
|
39
|
+
exports.keyRestriction = keyRestriction;
|
|
40
|
+
/** Enough to recognise whose key this is without printing the whole blob. */
|
|
41
|
+
function summarizeKey(keyLine) {
|
|
42
|
+
let parts = keyLine.trim().split(/\s+/);
|
|
43
|
+
let typeIndex = parts.findIndex(part => /^(ssh-|ecdsa-|sk-)/.test(part));
|
|
44
|
+
if (typeIndex < 0) {
|
|
45
|
+
return keyLine.slice(0, 60);
|
|
46
|
+
}
|
|
47
|
+
let type = parts[typeIndex];
|
|
48
|
+
let blob = parts[typeIndex + 1] || "";
|
|
49
|
+
let comment = parts.slice(typeIndex + 2).join(" ");
|
|
50
|
+
return `${type} ...${blob.slice(-12)}${comment && ` ${comment}` || ""}`;
|
|
51
|
+
}
|
|
52
|
+
exports.summarizeKey = summarizeKey;
|
|
53
|
+
/** Reads the authorized keys a repo checkout wants applied. Prefers a top level authorized_keys
|
|
54
|
+
file and otherwise concatenates every .pub at the top level. */
|
|
55
|
+
async function readRepoKeys(repoPath) {
|
|
56
|
+
let combinedPath = path_1.default.join(repoPath, "authorized_keys");
|
|
57
|
+
let entries = await promises_1.default.readdir(repoPath);
|
|
58
|
+
if (entries.includes("authorized_keys")) {
|
|
59
|
+
return normalizeKeys(await promises_1.default.readFile(combinedPath, "utf8"));
|
|
60
|
+
}
|
|
61
|
+
let pubFiles = entries.filter(name => name.endsWith(".pub")).sort();
|
|
62
|
+
if (!pubFiles.length) {
|
|
63
|
+
throw new Error(`Expected authorized_keys or at least one .pub file in ${repoPath}, found neither`);
|
|
64
|
+
}
|
|
65
|
+
let keys = [];
|
|
66
|
+
for (let name of pubFiles) {
|
|
67
|
+
keys.push(...normalizeKeys(await promises_1.default.readFile(path_1.default.join(repoPath, name), "utf8")));
|
|
68
|
+
}
|
|
69
|
+
return keys;
|
|
70
|
+
}
|
|
71
|
+
exports.readRepoKeys = readRepoKeys;
|
|
72
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aG9yaXplZEtleXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhdXRob3JpemVkS2V5cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSxvREFBNEI7QUFDNUIsMkRBQTZCO0FBQzdCLGdEQUF3QjtBQUV4Qiw2R0FBNkc7QUFDN0csK0ZBQStGO0FBQy9GLGdHQUFnRztBQUNoRyxTQUFTO0FBRVQsU0FBZ0IsYUFBYSxDQUFDLFFBQWdCO0lBQzFDLE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7QUFDdkcsQ0FBQztBQUZELHNDQUVDO0FBRUQ7OEVBQzhFO0FBQzlFLFNBQWdCLGNBQWMsQ0FBQyxPQUFlO0lBQzFDLElBQUksS0FBSyxHQUFHLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDeEMsSUFBSSxTQUFTLEdBQUcsS0FBSyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLG9CQUFvQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ3pFLElBQUksSUFBSSxHQUFHLFNBQVMsSUFBSSxDQUFDLElBQUksS0FBSyxDQUFDLFNBQVMsR0FBRyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDeEQsSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO1FBQ1IsT0FBTyxFQUFFLENBQUM7SUFDZCxDQUFDO0lBQ0QsT0FBTyxTQUFTLEdBQUcsZ0JBQU0sQ0FBQyxVQUFVLENBQUMsUUFBUSxDQUFDLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFF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cHRvIGZyb20gXCJjcnlwdG9cIjtcbmltcG9ydCBmcyBmcm9tIFwiZnMvcHJvbWlzZXNcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbi8vIFBPUlRFRCBDT0RFOiBzZWN1cml0eS9hdXRob3JpemVkS2V5cy9kYWVtb24vcG9ydHNlY3VyZURhZW1vbi5qcyBjb250YWlucyBhIHBsYWluIEpTIHBvcnQgb2Ygbm9ybWFsaXplS2V5cyxcbi8vIHN1bW1hcml6ZUtleSBhbmQgcmVhZFJlcG9LZXlzLCBzbyBpdCBjYW4gcmVzb2x2ZSB0aGUgc2FtZSBrZXlzIHdpdGggbm8gZGVwZW5kZW5jaWVzLiBUaGUgdHdvXG4vLyBtdXN0IGFncmVlIG9uIHdoaWNoIGtleXMgYSByZXBvIHByb2R1Y2VzIC0gaWYgeW91IGNoYW5nZSBvbmUsIG1ha2UgdGhlIG1hdGNoaW5nIGNoYW5nZSBpbiB0aGVcbi8vIG90aGVyLlxuXG5leHBvcnQgZnVuY3Rpb24gbm9ybWFsaXplS2V5cyhjb250ZW50czogc3RyaW5nKSB7XG4gICAgcmV0dXJuIGNvbnRlbnRzLnNwbGl0KFwiXFxuXCIpLm1hcChsaW5lID0+IGxpbmUudHJpbSgpKS5maWx0ZXIobGluZSA9PiBsaW5lICYmICFsaW5lLnN0YXJ0c1dpdGgoXCIjXCIpKTtcbn1cblxuLyoqIFRoZSBmaW5nZXJwcmludCBzc2ggaXRzZWxmIHJlcG9ydHMgZm9yIGEga2V5LCB3aGljaCBpcyB3aGF0IHRoZSBzc2hkIGxvZyBuYW1lcyBhbmQgdGhlcmVmb3JlXG4gICAgd2hhdCBhIHJldm9jYXRpb24gaXMga2V5ZWQgYnkuIFJldHVybnMgXCJcIiBmb3IgYSBsaW5lIHRoYXQgaG9sZHMgbm8ga2V5LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGtleUZpbmdlcnByaW50KGtleUxpbmU6IHN0cmluZykge1xuICAgIGxldCBwYXJ0cyA9IGtleUxpbmUudHJpbSgpLnNwbGl0KC9cXHMrLyk7XG4gICAgbGV0IHR5cGVJbmRleCA9IHBhcnRzLmZpbmRJbmRleChwYXJ0ID0+IC9eKHNzaC18ZWNkc2EtfHNrLSkvLnRlc3QocGFydCkpO1xuICAgIGxldCBibG9iID0gdHlwZUluZGV4ID49IDAgJiYgcGFydHNbdHlwZUluZGV4ICsgMV0gfHwgXCJcIjtcbiAgICBpZiAoIWJsb2IpIHtcbiAgICAgICAgcmV0dXJuIFwiXCI7XG4gICAgfVxuICAgIHJldHVybiBcIlNIQTI1NjpcIiArIGNyeXB0by5jcmVhdGVIYXNoKFwic2hhMjU2XCIpLnVwZGF0ZShCdWZmZXIuZnJvbShibG9iLCBcImJhc2U2NFwiKSkuZGlnZXN0KFwiYmFzZTY0XCIpLnJlcGxhY2UoLz0rJC8sIFwiXCIpO1xufVxuXG4vKiogVGhlIGFkZHJlc3NlcyBhIGtleSBtYXkgYmUgdXNlZCBmcm9tLCB3aGljaCBpcyB0aGUgcGFydCBvZiBhbiBhdXRob3JpemVkX2tleXMgbGluZSB0aGF0XG4gICAgZGVjaWRlcyBob3cgbXVjaCBhIHN0b2xlbiBrZXkgaXMgd29ydGguIEEga2V5IHdpdGggbm8gcmVzdHJpY3Rpb24gc2F5cyBzbyBsb3VkbHkuICovXG5leHBvcnQgZnVuY3Rpb24ga2V5UmVzdHJpY3Rpb24oa2V5TGluZTogc3RyaW5nKSB7XG4gICAgbGV0IG1hdGNoID0ga2V5TGluZS5tYXRjaCgvZnJvbT1cIihbXlwiXSopXCIvKTtcbiAgICBpZiAoIW1hdGNoKSB7XG4gICAgICAgIHJldHVybiBcIkFOWSBBRERSRVNTIChubyBmcm9tPSByZXN0cmljdGlvbilcIjtcbiAgICB9XG4gICAgcmV0dXJuIG1hdGNoWzFdO1xufVxuXG4vKiogRW5vdWdoIHRvIHJlY29nbmlzZSB3aG9zZSBrZXkgdGhpcyBpcyB3aXRob3V0IHByaW50aW5nIHRoZSB3aG9sZSBibG9iLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHN1bW1hcml6ZUtleShrZXlMaW5lOiBzdHJpbmcpIHtcbiAgICBsZXQgcGFydHMgPSBrZXlMaW5lLnRyaW0oKS5zcGxpdCgvXFxzKy8pO1xuICAgIGxldCB0eXBlSW5kZXggPSBwYXJ0cy5maW5kSW5kZXgocGFydCA9PiAvXihzc2gtfGVjZHNhLXxzay0pLy50ZXN0KHBhcnQpKTtcbiAgICBpZiAodHlwZUluZGV4IDwgMCkge1xuICAgICAgICByZXR1cm4ga2V5TGluZS5zbGljZSgwLCA2MCk7XG4gICAgfVxuICAgIGxldCB0eXBlID0gcGFydHNbdHlwZUluZGV4XTtcbiAgICBsZXQgYmxvYiA9IHBhcnRzW3R5cGVJbmRleCArIDFdIHx8IFwiXCI7XG4gICAgbGV0IGNvbW1lbnQgPSBwYXJ0cy5zbGljZSh0eXBlSW5kZXggKyAyKS5qb2luKFwiIFwiKTtcbiAgICByZXR1cm4gYCR7dHlwZX0gLi4uJHtibG9iLnNsaWNlKC0xMil9JHtjb21tZW50ICYmIGAgJHtjb21tZW50fWAgfHwgXCJcIn1gO1xufVxuXG4vKiogUmVhZHMgdGhlIGF1dGhvcml6ZWQga2V5cyBhIHJlcG8gY2hlY2tvdXQgd2FudHMgYXBwbGllZC4gUHJlZmVycyBhIHRvcCBsZXZlbCBhdXRob3JpemVkX2tleXNcbiAgICBmaWxlIGFuZCBvdGhlcndpc2UgY29uY2F0ZW5hdGVzIGV2ZXJ5IC5wdWIgYXQgdGhlIHRvcCBsZXZlbC4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZWFkUmVwb0tleXMocmVwb1BhdGg6IHN0cmluZykge1xuICAgIGxldCBjb21iaW5lZFBhdGggPSBwYXRoLmpvaW4ocmVwb1BhdGgsIFwiYXV0aG9yaXplZF9rZXlzXCIpO1xuICAgIGxldCBlbnRyaWVzID0gYXdhaXQgZnMucmVhZGRpcihyZXBvUGF0aCk7XG4gICAgaWYgKGVudHJpZXMuaW5jbHVkZXMoXCJhdXRob3JpemVkX2tleXNcIikpIHtcbiAgICAgICAgcmV0dXJuIG5vcm1hbGl6ZUtleXMoYXdhaXQgZnMucmVhZEZpbGUoY29tYmluZWRQYXRoLCBcInV0ZjhcIikpO1xuICAgIH1cbiAgICBsZXQgcHViRmlsZXMgPSBlbnRyaWVzLmZpbHRlcihuYW1lID0+IG5hbWUuZW5kc1dpdGgoXCIucHViXCIpKS5zb3J0KCk7XG4gICAgaWYgKCFwdWJGaWxlcy5sZW5ndGgpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBhdXRob3JpemVkX2tleXMgb3IgYXQgbGVhc3Qgb25lIC5wdWIgZmlsZSBpbiAke3JlcG9QYXRofSwgZm91bmQgbmVpdGhlcmApO1xuICAgIH1cbiAgICBsZXQga2V5czogc3RyaW5nW10gPSBbXTtcbiAgICBmb3IgKGxldCBuYW1lIG9mIHB1YkZpbGVzKSB7XG4gICAgICAgIGtleXMucHVzaCguLi5ub3JtYWxpemVLZXlzKGF3YWl0IGZzLnJlYWRGaWxlKHBhdGguam9pbihyZXBvUGF0aCwgbmFtZSksIFwidXRmOFwiKSkpO1xuICAgIH1cbiAgICByZXR1cm4ga2V5cztcbn1cbiJdfQ==
|
|
73
|
+
/* _JS_SOURCE_HASH = "acd843c08a7ede6419089a90a105d6fbe56220931b66deb6b231678bdebe43cf"; */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
|
+
//exports.deriveRevokeKey = exports.revokeRepoPath = exports.revokeKeyPath = exports.revokeRepoURL = exports.REVOKE_KEY_LABEL = void 0;
|
|
4
|
+
const deriveKey_1 = require("../keys/deriveKey");
|
|
5
|
+
const sshKeyFile_1 = require("../keys/sshKeyFile");
|
|
6
|
+
const sources_1 = require("./sources");
|
|
7
|
+
// A source's revocations live in a repo beside it. Derived rather than configured, so adding a
|
|
8
|
+
// source brings its revoke repo with it and there is nothing extra to pass on the command line.
|
|
9
|
+
const REVOKED_SUFFIX = "_revoked";
|
|
10
|
+
// GitHub will not take the same public key as a deploy key on two repos, so the revoke repo is
|
|
11
|
+
// reached with a key derived from the source's, under this label.
|
|
12
|
+
exports.REVOKE_KEY_LABEL = "revokegithubkey";
|
|
13
|
+
function revokeRepoURL(sourceURL) {
|
|
14
|
+
let withoutGit = sourceURL.replace(/\.git$/, "");
|
|
15
|
+
return `${withoutGit}${REVOKED_SUFFIX}.git`;
|
|
16
|
+
}
|
|
17
|
+
exports.revokeRepoURL = revokeRepoURL;
|
|
18
|
+
function revokeKeyPath(sourceURL) {
|
|
19
|
+
return `${sources_1.REPO_KEYS_DIR}/${(0, sources_1.sourceName)(sourceURL)}${REVOKED_SUFFIX}`;
|
|
20
|
+
}
|
|
21
|
+
exports.revokeKeyPath = revokeKeyPath;
|
|
22
|
+
function revokeRepoPath(sourceURL) {
|
|
23
|
+
return `${sources_1.REPOS_DIR}/${(0, sources_1.sourceName)(sourceURL)}${REVOKED_SUFFIX}`;
|
|
24
|
+
}
|
|
25
|
+
exports.revokeRepoPath = revokeRepoPath;
|
|
26
|
+
/** The revoke repo's key, worked out from the source repo's key. Anything holding the source key
|
|
27
|
+
can produce it, so it never has to be stored anywhere separately or handed around. */
|
|
28
|
+
function deriveRevokeKey(sourcePrivateKey) {
|
|
29
|
+
let source = (0, sshKeyFile_1.parseOpenSSHPrivateKey)(sourcePrivateKey);
|
|
30
|
+
let derived = (0, deriveKey_1.deriveEd25519Key)({ seed: source.seed, label: exports.REVOKE_KEY_LABEL });
|
|
31
|
+
return {
|
|
32
|
+
publicKey: `ssh-ed25519 ${Buffer.concat([
|
|
33
|
+
Buffer.from([0, 0, 0, 11]), Buffer.from("ssh-ed25519"),
|
|
34
|
+
Buffer.from([0, 0, 0, 32]), derived.publicKey,
|
|
35
|
+
]).toString("base64")}`,
|
|
36
|
+
privateKeyFile: (0, sshKeyFile_1.formatOpenSSHPrivateKey)({
|
|
37
|
+
seed: derived.seed,
|
|
38
|
+
comment: `${source.comment || "portsecure"} ${exports.REVOKE_KEY_LABEL}`,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
exports.deriveRevokeKey = deriveRevokeKey;
|
|
43
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmV2b2tlU291cmNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicmV2b2tlU291cmNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLGlEQUFxRDtBQUNyRCxtREFBcUY7QUFDckYsdUNBQWlFO0FBRWpFLCtGQUErRjtBQUMvRixnR0FBZ0c7QUFDaEcsTUFBTSxjQUFjLEdBQUcsVUFBVSxDQUFDO0FBQ2xDLCtGQUErRjtBQUMvRixrRUFBa0U7QUFDckQsUUFBQSxnQkFBZ0IsR0FBRyxpQkFBaUIsQ0FBQztBQUVsRCxTQUFnQixhQUFhLENBQUMsU0FBaUI7SUFDM0MsSUFBSSxVQUFVLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDakQsT0FBTyxHQUFHLFVBQVUsR0FBRyxjQUFjLE1BQU0sQ0FBQztBQUNoRCxDQUFDO0FBSEQsc0NBR0M7QUFFRCxTQUFnQixhQUFhLENBQUMsU0FBaUI7SUFDM0MsT0FBTyxHQUFHLHVCQUFhLElBQUksSUFBQSxvQkFBVSxFQUFDLFNBQVMsQ0FBQyxHQUFHLGNBQWMsRUFBRSxDQUFDO0FBQ3hFLENBQUM7QUFGRCxzQ0FFQztBQUVELFNBQWdCLGNBQWMsQ0FBQyxTQUFpQjtJQUM1QyxPQUFPLEdBQUcsbUJBQVMsSUFBSSxJQUFBLG9CQUFVLEVBQUMsU0FBUyxDQUFDLEdBQUcsY0FBYyxFQUFFLENBQUM7QUFDcEUsQ0FBQztBQUZELHdDQUVDO0FBRUQ7eUZBQ3lGO0FBQ3pGLFNBQWdCLGVBQWUsQ0FBQyxnQkFBd0I7SUFDcEQsSUFBSSxNQUFNLEdBQUcsSUFBQSxtQ0FBc0IsRUFBQyxnQkFBZ0IsQ0FBQyxDQUFDO0lBQ3RELElBQUksT0FBTyxHQUFHLElBQUEsNEJBQWdCLEVBQUMsRUFBRSxJQUFJLEVBQUUsTUFBTSxDQUFDLElBQUksRUFBRSxLQUFLLEVBQUUsd0JBQWdCLEVBQUUsQ0FBQyxDQUFDO0lBQy9FLE9BQU87UUFDSCxTQUFTLEVBQUUsZUFBZSxNQUFNLENBQUMsTUFBTSxDQUFDO1lBQ3BDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDO1lBQ3RELE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxFQUFFLE9BQU8sQ0FBQyxTQUFTO1NBQ2hELENBQUMsQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLEVBQUU7UUFDdkIsY0FBYyxFQUFFLElBQUEsb0NBQXVCLEVBQUM7WUFDcEMsSUFBSSxFQUFFLE9BQU8sQ0FBQyxJQUFJO1lBQ2xCLE9BQU8sRUFBRSxHQUFHLE1BQU0sQ0FBQyxPQUFPLElBQUksWUFBWSxJQUFJLHdCQUFnQixFQUFFO1NBQ25FLENBQUM7S0FDTCxDQUFDO0FBQ04sQ0FBQztBQWJELDBDQWFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgZGVyaXZlRWQyNTUxOUtleSB9IGZyb20gXCIuLi9rZXlzL2Rlcml2ZUtleVwiO1xuaW1wb3J0IHsgZm9ybWF0T3BlblNTSFByaXZhdGVLZXksIHBhcnNlT3BlblNTSFByaXZhdGVLZXkgfSBmcm9tIFwiLi4va2V5cy9zc2hLZXlGaWxlXCI7XG5pbXBvcnQgeyBSRVBPX0tFWVNfRElSLCBSRVBPU19ESVIsIHNvdXJjZU5hbWUgfSBmcm9tIFwiLi9zb3VyY2VzXCI7XG5cbi8vIEEgc291cmNlJ3MgcmV2b2NhdGlvbnMgbGl2ZSBpbiBhIHJlcG8gYmVzaWRlIGl0LiBEZXJpdmVkIHJhdGhlciB0aGFuIGNvbmZpZ3VyZWQsIHNvIGFkZGluZyBhXG4vLyBzb3VyY2UgYnJpbmdzIGl0cyByZXZva2UgcmVwbyB3aXRoIGl0IGFuZCB0aGVyZSBpcyBub3RoaW5nIGV4dHJhIHRvIHBhc3Mgb24gdGhlIGNvbW1hbmQgbGluZS5cbmNvbnN0IFJFVk9LRURfU1VGRklYID0gXCJfcmV2b2tlZFwiO1xuLy8gR2l0SHViIHdpbGwgbm90IHRha2UgdGhlIHNhbWUgcHVibGljIGtleSBhcyBhIGRlcGxveSBrZXkgb24gdHdvIHJlcG9zLCBzbyB0aGUgcmV2b2tlIHJlcG8gaXNcbi8vIHJlYWNoZWQgd2l0aCBhIGtleSBkZXJpdmVkIGZyb20gdGhlIHNvdXJjZSdzLCB1bmRlciB0aGlzIGxhYmVsLlxuZXhwb3J0IGNvbnN0IFJFVk9LRV9LRVlfTEFCRUwgPSBcInJldm9rZWdpdGh1YmtleVwiO1xuXG5leHBvcnQgZnVuY3Rpb24gcmV2b2tlUmVwb1VSTChzb3VyY2VVUkw6IHN0cmluZykge1xuICAgIGxldCB3aXRob3V0R2l0ID0gc291cmNlVVJMLnJlcGxhY2UoL1xcLmdpdCQvLCBcIlwiKTtcbiAgICByZXR1cm4gYCR7d2l0aG91dEdpdH0ke1JFVk9LRURfU1VGRklYfS5naXRgO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gcmV2b2tlS2V5UGF0aChzb3VyY2VVUkw6IHN0cmluZykge1xuICAgIHJldHVybiBgJHtSRVBPX0tFWVNfRElSfS8ke3NvdXJjZU5hbWUoc291cmNlVVJMKX0ke1JFVk9LRURfU1VGRklYfWA7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZXZva2VSZXBvUGF0aChzb3VyY2VVUkw6IHN0cmluZykge1xuICAgIHJldHVybiBgJHtSRVBPU19ESVJ9LyR7c291cmNlTmFtZShzb3VyY2VVUkwpfSR7UkVWT0tFRF9TVUZGSVh9YDtcbn1cblxuLyoqIFRoZSByZXZva2UgcmVwbydzIGtleSwgd29ya2VkIG91dCBmcm9tIHRoZSBzb3VyY2UgcmVwbydzIGtleS4gQW55dGhpbmcgaG9sZGluZyB0aGUgc291cmNlIGtleVxuICAgIGNhbiBwcm9kdWNlIGl0LCBzbyBpdCBuZXZlciBoYXMgdG8gYmUgc3RvcmVkIGFueXdoZXJlIHNlcGFyYXRlbHkgb3IgaGFuZGVkIGFyb3VuZC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBkZXJpdmVSZXZva2VLZXkoc291cmNlUHJpdmF0ZUtleTogc3RyaW5nKSB7XG4gICAgbGV0IHNvdXJjZSA9IHBhcnNlT3BlblNTSFByaXZhdGVLZXkoc291cmNlUHJpdmF0ZUtleSk7XG4gICAgbGV0IGRlcml2ZWQgPSBkZXJpdmVFZDI1NTE5S2V5KHsgc2VlZDogc291cmNlLnNlZWQsIGxhYmVsOiBSRVZPS0VfS0VZX0xBQkVMIH0pO1xuICAgIHJldHVybiB7XG4gICAgICAgIHB1YmxpY0tleTogYHNzaC1lZDI1NTE5ICR7QnVmZmVyLmNvbmNhdChbXG4gICAgICAgICAgICBCdWZmZXIuZnJvbShbMCwgMCwgMCwgMTFdKSwgQnVmZmVyLmZyb20oXCJzc2gtZWQyNTUxOVwiKSxcbiAgICAgICAgICAgIEJ1ZmZlci5mcm9tKFswLCAwLCAwLCAzMl0pLCBkZXJpdmVkLnB1YmxpY0tleSxcbiAgICAgICAgXSkudG9TdHJpbmcoXCJiYXNlNjRcIil9YCxcbiAgICAgICAgcHJpdmF0ZUtleUZpbGU6IGZvcm1hdE9wZW5TU0hQcml2YXRlS2V5KHtcbiAgICAgICAgICAgIHNlZWQ6IGRlcml2ZWQuc2VlZCxcbiAgICAgICAgICAgIGNvbW1lbnQ6IGAke3NvdXJjZS5jb21tZW50IHx8IFwicG9ydHNlY3VyZVwifSAke1JFVk9LRV9LRVlfTEFCRUx9YCxcbiAgICAgICAgfSksXG4gICAgfTtcbn1cbiJdfQ==
|
|
44
|
+
/* _JS_SOURCE_HASH = "9f16cd6aea6e1f1a646eda715ac4cac70dc67579ce23c1e473bd371b7985b229"; */
|