sliftutils 1.7.126 → 1.7.127
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/bin/testnotify.js +9 -0
- package/package.json +5 -3
- package/security/authorizedKeys/authorizedKeys.ts +49 -9
- package/security/authorizedKeys/daemon/authLog.ts +74 -5
- package/security/authorizedKeys/daemon/changes.ts +26 -0
- package/security/authorizedKeys/daemon/daemon.ts +51 -24
- package/security/authorizedKeys/daemon/revocation.ts +46 -23
- package/security/authorizedKeys/daemon/rootKeys.ts +88 -25
- package/security/authorizedKeys/daemon/sessions.ts +139 -0
- package/security/authorizedKeys/daemon/state.ts +16 -2
- package/security/authorizedKeys/dist/authorizedKeys.ts.cache +49 -11
- package/security/authorizedKeys/dist/secureSSH.ts.cache +18 -31
- package/security/authorizedKeys/dist/unrevoke.ts.cache +36 -14
- package/security/authorizedKeys/secureSSH.ts +77 -61
- package/security/authorizedKeys/unrevoke.ts +38 -12
- package/security/helpers/remoteSSH.ts +17 -6
- package/security/notifications/discord.ts +18 -1
- package/security/notifications/dist/discord.ts.cache +21 -4
- package/security/notifications/setupNotify.ts +17 -8
- package/security/notifications/testNotify.ts +48 -0
- package/security/signedFiles/dist/manifest.ts.cache +21 -4
- package/security/signedFiles/dist/signFiles.ts.cache +48 -13
- package/security/signedFiles/manifest.ts +19 -2
- package/security/signedFiles/signFiles.ts +51 -12
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { keyFingerprint, keyRestriction, summarizeKey } from "../authorizedKeys";
|
|
3
|
+
import { keyFingerprint, keyRestriction, keyRestrictionList, NO_RESTRICTION, summarizeKey } from "../authorizedKeys";
|
|
4
4
|
import { KEYS_HISTORY_PATH, ROOT_AUTHORIZED_KEYS } from "./paths";
|
|
5
5
|
import { notify } from "./notify";
|
|
6
|
+
import { getState, saveState } from "./state";
|
|
7
|
+
import { takeChangeReasons } from "./changes";
|
|
6
8
|
|
|
7
9
|
const KEY_FILE_HEADER = "# Managed by portsecure. Manual changes are reverted and reported.";
|
|
8
10
|
|
|
@@ -22,6 +24,35 @@ function keyIdentity(keyLine: string) {
|
|
|
22
24
|
return keyFingerprint(keyLine) || keyLine;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** Which addresses a key gained or lost, rather than two long lists to compare by eye. Returns ""
|
|
28
|
+
when the addresses did not change at all. Losing the from= altogether is the loudest case there
|
|
29
|
+
is, so it is spelled out rather than shown as a list of removals. */
|
|
30
|
+
function describeAddressChange(config: { previousLine: string; currentLine: string }) {
|
|
31
|
+
let { previousLine, currentLine } = config;
|
|
32
|
+
let previous = keyRestrictionList(previousLine);
|
|
33
|
+
let current = keyRestrictionList(currentLine);
|
|
34
|
+
if (!previous && !current) {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
if (previous && !current) {
|
|
38
|
+
return ` was ${previous.join(",")}\n now ${NO_RESTRICTION}`;
|
|
39
|
+
}
|
|
40
|
+
if (!previous && current) {
|
|
41
|
+
return ` was ${NO_RESTRICTION}\n now restricted to ${current.join(",")}`;
|
|
42
|
+
}
|
|
43
|
+
let removed = (previous || []).filter(address => !(current || []).includes(address));
|
|
44
|
+
let added = (current || []).filter(address => !(previous || []).includes(address));
|
|
45
|
+
if (!removed.length && !added.length) {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
let lines = [
|
|
49
|
+
...removed.map(address => ` no longer allowed from ${address}`),
|
|
50
|
+
...added.map(address => ` now also allowed from ${address}`),
|
|
51
|
+
];
|
|
52
|
+
lines.push(` still allowed from ${(current || []).filter(address => !added.includes(address)).join(",") || "nothing"}`);
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
25
56
|
export function describeKeyDifference(config: { before: string[]; after: string[] }) {
|
|
26
57
|
let { before, after } = config;
|
|
27
58
|
let beforeByKey = new Map(before.map(key => [keyIdentity(key), key]));
|
|
@@ -43,17 +74,13 @@ export function describeKeyDifference(config: { before: string[]; after: string[
|
|
|
43
74
|
if (!currentLine || currentLine === previousLine) {
|
|
44
75
|
continue;
|
|
45
76
|
}
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
lines.push(
|
|
50
|
-
`~ changed ${summarizeKey(currentLine)}\n from ${previousFrom}\n`
|
|
51
|
-
+ ` to ${currentFrom}`
|
|
52
|
-
);
|
|
77
|
+
let addressChange = describeAddressChange({ previousLine, currentLine });
|
|
78
|
+
if (addressChange) {
|
|
79
|
+
lines.push(`~ changed ${summarizeKey(currentLine)}\n${addressChange}`);
|
|
53
80
|
continue;
|
|
54
81
|
}
|
|
55
82
|
lines.push(
|
|
56
|
-
`~ changed ${summarizeKey(currentLine)}\n from ${
|
|
83
|
+
`~ changed ${summarizeKey(currentLine)}\n from ${keyRestriction(currentLine)}\n`
|
|
57
84
|
+ ` its options or comment changed`
|
|
58
85
|
);
|
|
59
86
|
}
|
|
@@ -106,30 +133,66 @@ export async function archiveAuthorizedKeys(config: { filePath: string; reason:
|
|
|
106
133
|
return archivePath;
|
|
107
134
|
}
|
|
108
135
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
function sameKeys(one: string[], two: string[]) {
|
|
137
|
+
return one.length === two.length && one.every((key, index) => key === two[index]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Puts the allowed keys in place, and says which of the two things happened.
|
|
141
|
+
|
|
142
|
+
Whether this was our own doing is decided by comparing what we want against what we last wrote,
|
|
143
|
+
not by guessing from whether a repo happened to change. A revocation changes what we want
|
|
144
|
+
without any repo changing, and reading that as somebody having edited the file was both wrong
|
|
145
|
+
and alarming. */
|
|
146
|
+
export async function enforceRootKeys(keys: string[]) {
|
|
147
|
+
// An empty set is written out like any other. If every key is revoked then nobody should be
|
|
148
|
+
// getting in, and the way back is to put a key in the repo, which is already being watched.
|
|
112
149
|
if (!keys.length) {
|
|
113
|
-
|
|
114
|
-
// whatever access is already in place stays exactly as it is.
|
|
115
|
-
console.log(`No keys came from any source, leaving ${ROOT_AUTHORIZED_KEYS} as it is`);
|
|
116
|
-
return;
|
|
150
|
+
console.log(`No keys are allowed, so ${ROOT_AUTHORIZED_KEYS} is being emptied`);
|
|
117
151
|
}
|
|
152
|
+
let state = getState();
|
|
118
153
|
let currentKeys = await readAuthorizedKeysFile(ROOT_AUTHORIZED_KEYS);
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
if (!state.appliedKeys.length) {
|
|
155
|
+
// Nothing recorded yet, on a first start or an upgrade. Whatever is in the file is taken as
|
|
156
|
+
// ours, so the first check reports what actually changes rather than reintroducing every
|
|
157
|
+
// key that was already there.
|
|
158
|
+
state.appliedKeys = currentKeys;
|
|
159
|
+
await saveState();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (sameKeys(currentKeys, keys)) {
|
|
163
|
+
// Nothing came of whatever was decided this pass, so none of it is reported.
|
|
164
|
+
takeChangeReasons();
|
|
165
|
+
if (!sameKeys(state.appliedKeys, keys)) {
|
|
166
|
+
state.appliedKeys = keys;
|
|
167
|
+
await saveState();
|
|
168
|
+
}
|
|
121
169
|
return;
|
|
122
170
|
}
|
|
123
|
-
|
|
171
|
+
|
|
172
|
+
let weChangedIt = !sameKeys(state.appliedKeys, keys);
|
|
173
|
+
let previouslyApplied = state.appliedKeys;
|
|
174
|
+
// Still archived, it is just not worth a line in the message. Whoever needs the old file knows
|
|
175
|
+
// where the history is.
|
|
176
|
+
await archiveAuthorizedKeys({
|
|
177
|
+
filePath: ROOT_AUTHORIZED_KEYS,
|
|
178
|
+
reason: weChangedIt && "update" || "reverted",
|
|
179
|
+
});
|
|
124
180
|
await writeAuthorizedKeysFile({ filePath: ROOT_AUTHORIZED_KEYS, keys });
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
181
|
+
state.appliedKeys = keys;
|
|
182
|
+
await saveState();
|
|
183
|
+
|
|
184
|
+
let reasons = takeChangeReasons();
|
|
185
|
+
let why = reasons.length && `\n\n${reasons.join("\n\n")}` || "";
|
|
186
|
+
if (weChangedIt) {
|
|
187
|
+
await notify(
|
|
188
|
+
`applied authorized key changes:`
|
|
189
|
+
+ `\n\`\`\`\n${describeKeyDifference({ before: previouslyApplied, after: keys })}\n\`\`\`${why}`
|
|
190
|
+
);
|
|
129
191
|
return;
|
|
130
192
|
}
|
|
131
193
|
await notify(
|
|
132
|
-
`root's authorized_keys was
|
|
133
|
-
+
|
|
194
|
+
`root's authorized_keys was edited by something other than portsecure. The edit below has`
|
|
195
|
+
+ ` been undone, and the keys from the repos are back in place.`
|
|
196
|
+
+ `\n\`\`\`\n${describeKeyDifference({ before: keys, after: currentKeys })}\n\`\`\`${why}`
|
|
134
197
|
);
|
|
135
198
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { spawnPromise } from "../../helpers/spawn";
|
|
3
|
+
import { AUTH_LOG_PATH } from "./paths";
|
|
4
|
+
|
|
5
|
+
// Taking a key out of authorized_keys only stops the next login. Whoever is already connected with
|
|
6
|
+
// it stays connected for as long as they like, which would make revoking a key that is being
|
|
7
|
+
// actively misused close to pointless.
|
|
8
|
+
//
|
|
9
|
+
// sshd names the key it accepted and the process handling that connection on the same line, so the
|
|
10
|
+
// sessions belonging to one key can be ended without touching anyone else's.
|
|
11
|
+
const ACCEPTED = /(?:sshd|sshd-session)\[(\d+)\]:\s+Accepted publickey for (\S+) from (\S+) port (\d+) ssh2: \S+ (SHA256:[A-Za-z0-9+/=]+)/;
|
|
12
|
+
|
|
13
|
+
export type KeySession = { processId: number; user: string; ip: string; port: string };
|
|
14
|
+
|
|
15
|
+
export function parseAcceptedSessions(contents: string, fingerprint: string) {
|
|
16
|
+
let sessions: KeySession[] = [];
|
|
17
|
+
for (let line of contents.split("\n")) {
|
|
18
|
+
let match = line.match(ACCEPTED);
|
|
19
|
+
if (!match || match[5] !== fingerprint) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
sessions.push({ processId: Number(match[1]), user: match[2], ip: match[3], port: match[4] });
|
|
23
|
+
}
|
|
24
|
+
return sessions;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Process ids are reused, so one is only killed when it is still an ssh session. Killing whatever
|
|
28
|
+
happens to hold that number now would be far worse than missing a session. */
|
|
29
|
+
async function isSSHSession(processId: number) {
|
|
30
|
+
try {
|
|
31
|
+
let name = await fs.readFile(`/proc/${processId}/comm`, "utf8");
|
|
32
|
+
return name.trim().startsWith("sshd");
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The whole log, not only what is new, because the session being ended may have been established
|
|
39
|
+
long before the key was misused. */
|
|
40
|
+
async function readWholeAuthLog() {
|
|
41
|
+
try {
|
|
42
|
+
return await fs.readFile(AUTH_LOG_PATH, "utf8");
|
|
43
|
+
} catch (e) {
|
|
44
|
+
let result = await spawnPromise({
|
|
45
|
+
command: "journalctl",
|
|
46
|
+
args: ["-u", "ssh", "-u", "sshd", "--no-pager", "--since", "-30days"],
|
|
47
|
+
});
|
|
48
|
+
return result.status === 0 && result.stdout || "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Ends every live session that authenticated with this key. Returns what was ended, so the
|
|
53
|
+
revocation can say so rather than leaving it to be discovered. */
|
|
54
|
+
export async function endSessionsUsingKey(fingerprint: string) {
|
|
55
|
+
let ended: KeySession[] = [];
|
|
56
|
+
for (let session of parseAcceptedSessions(await readWholeAuthLog(), fingerprint)) {
|
|
57
|
+
if (session.processId <= 1 || session.processId === process.pid) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!await isSSHSession(session.processId)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
process.kill(session.processId, "SIGKILL");
|
|
65
|
+
ended.push(session);
|
|
66
|
+
console.log(`Ended ssh session ${session.processId} (${session.user} from ${session.ip}), it used ${fingerprint}`);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// Gone between looking and killing, which is the outcome we wanted anyway.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return ended;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether this process is one connection. The listener is not a connection - it is the process
|
|
75
|
+
that accepts them - so it is not a candidate here at all.
|
|
76
|
+
|
|
77
|
+
Every openssh since 9.8 runs each connection as its own sshd-session. On older ones connections
|
|
78
|
+
are named sshd too, and the listener is the one systemd started directly with -D. */
|
|
79
|
+
async function isConnectionProcess(processId: number) {
|
|
80
|
+
let name: string;
|
|
81
|
+
try {
|
|
82
|
+
name = (await fs.readFile(`/proc/${processId}/comm`, "utf8")).trim();
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (name === "sshd-session") {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (name !== "sshd") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
let commandLine = await fs.readFile(`/proc/${processId}/cmdline`, "utf8").catch(() => "");
|
|
93
|
+
let parentId = Number((await fs.readFile(`/proc/${processId}/stat`, "utf8").catch(() => "")).split(") ")[1]?.split(" ")[1] || 0);
|
|
94
|
+
return !commandLine.includes("-D") && parentId !== 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Disconnects every ssh session on the machine.
|
|
98
|
+
|
|
99
|
+
Not only the ones using the key that was just taken away: a session that predates the key being
|
|
100
|
+
removed is exactly as dangerous, and working out which sessions are still entitled to be here
|
|
101
|
+
is guesswork. Anyone who still has access can reconnect in a second, and anyone who does not
|
|
102
|
+
should not be here. */
|
|
103
|
+
export async function endAllSSHSessions() {
|
|
104
|
+
let ended: number[] = [];
|
|
105
|
+
for (let entry of await fs.readdir("/proc")) {
|
|
106
|
+
let processId = Number(entry);
|
|
107
|
+
if (!processId || processId <= 1 || processId === process.pid) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!await isConnectionProcess(processId)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
process.kill(processId, "SIGKILL");
|
|
115
|
+
ended.push(processId);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// Gone between looking and killing, which is the outcome we wanted anyway.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (ended.length) {
|
|
121
|
+
console.log(`Disconnected ${ended.length} ssh session(s): ${ended.join(", ")}`);
|
|
122
|
+
}
|
|
123
|
+
return ended;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function describeAllEnded(ended: number[]) {
|
|
127
|
+
if (!ended.length) {
|
|
128
|
+
return " Nothing was connected.";
|
|
129
|
+
}
|
|
130
|
+
return ` Every ssh session on this machine was disconnected, ${ended.length} of them.`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function describeEndedSessions(ended: KeySession[]) {
|
|
134
|
+
if (!ended.length) {
|
|
135
|
+
return " Nothing was connected with it.";
|
|
136
|
+
}
|
|
137
|
+
return ` ${ended.length} live session(s) using it were killed: `
|
|
138
|
+
+ ended.map(session => `${session.user}@${session.ip}:${session.port}`).join(", ") + ".";
|
|
139
|
+
}
|
|
@@ -45,8 +45,16 @@ export type PendingRevocation = {
|
|
|
45
45
|
export type DaemonState = {
|
|
46
46
|
sources: { [repoURL: string]: SourceState };
|
|
47
47
|
userKeyHashes: { [userName: string]: string };
|
|
48
|
+
// Held in memory only, never written to disk. Deleting a revocation from the revoke repo does
|
|
49
|
+
// not bring a key back, because this machine still remembers it - the key that writes
|
|
50
|
+
// revocations is on every server, so whoever stole one could otherwise erase the record that
|
|
51
|
+
// shut them out. Restarting the daemon does clear it, which is the way back from a revocation
|
|
52
|
+
// that should not have happened.
|
|
48
53
|
revocations: { [fingerprint: string]: RevocationState };
|
|
49
54
|
pendingRevocations: PendingRevocation[];
|
|
55
|
+
// The keys we last wrote out. What tells a change we made apart from one somebody else made:
|
|
56
|
+
// if this still matches what we want, and the file does not, the file was edited behind us.
|
|
57
|
+
appliedKeys: string[];
|
|
50
58
|
// Where we had read up to in the auth log, so a restart does not re-report old attempts.
|
|
51
59
|
authLogOffset: number;
|
|
52
60
|
authLogSignature: string;
|
|
@@ -57,6 +65,7 @@ let state: DaemonState = {
|
|
|
57
65
|
userKeyHashes: {},
|
|
58
66
|
revocations: {},
|
|
59
67
|
pendingRevocations: [],
|
|
68
|
+
appliedKeys: [],
|
|
60
69
|
authLogOffset: 0,
|
|
61
70
|
authLogSignature: "",
|
|
62
71
|
};
|
|
@@ -95,7 +104,11 @@ export async function loadState() {
|
|
|
95
104
|
return;
|
|
96
105
|
}
|
|
97
106
|
try {
|
|
98
|
-
|
|
107
|
+
let loaded = JSON.parse(contents);
|
|
108
|
+
// Revocations are held in memory only, so whatever an older version left on disk is not
|
|
109
|
+
// read back in. Starting the daemon is what forgets them.
|
|
110
|
+
delete loaded.revocations;
|
|
111
|
+
state = Object.assign(state, loaded);
|
|
99
112
|
} catch (e) {
|
|
100
113
|
// Corrupt state only costs us one duplicate notification, so it is not worth failing over.
|
|
101
114
|
console.log(`Ignoring unreadable state file ${STATE_PATH}. ${e}`);
|
|
@@ -103,6 +116,7 @@ export async function loadState() {
|
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
export async function saveState() {
|
|
119
|
+
let { revocations, ...persisted } = state;
|
|
106
120
|
await fs.mkdir(path.dirname(STATE_PATH), { recursive: true });
|
|
107
|
-
await fs.writeFile(STATE_PATH, JSON.stringify(
|
|
121
|
+
await fs.writeFile(STATE_PATH, JSON.stringify(persisted, undefined, 4), { mode: 0o600 });
|
|
108
122
|
}
|
|
@@ -3,14 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
6
|
-
//exports.readRepoKeys = exports.summarizeKey = exports.keyRestriction = exports.keyFingerprint = exports.normalizeKeys = void 0;
|
|
6
|
+
//exports.readRepoKeys = exports.findKeyProblems = exports.summarizeKey = exports.keyRestriction = exports.keyRestrictionList = exports.NO_RESTRICTION = exports.keyFingerprint = exports.normalizeKeys = void 0;
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
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
10
|
function normalizeKeys(contents) {
|
|
15
11
|
return contents.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("#"));
|
|
16
12
|
}
|
|
@@ -27,14 +23,25 @@ function keyFingerprint(keyLine) {
|
|
|
27
23
|
return "SHA256:" + crypto_1.default.createHash("sha256").update(Buffer.from(blob, "base64")).digest("base64").replace(/=+$/, "");
|
|
28
24
|
}
|
|
29
25
|
exports.keyFingerprint = keyFingerprint;
|
|
26
|
+
exports.NO_RESTRICTION = "ANY ADDRESS (no from= restriction)";
|
|
27
|
+
/** The addresses a key may be used from, one by one, or undefined when the key carries no from=
|
|
28
|
+
at all. Undefined and an empty list are very different things, so they stay distinguishable. */
|
|
29
|
+
function keyRestrictionList(keyLine) {
|
|
30
|
+
let match = keyLine.match(/from="([^"]*)"/);
|
|
31
|
+
if (!match) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return match[1].split(",").map(entry => entry.trim()).filter(entry => entry);
|
|
35
|
+
}
|
|
36
|
+
exports.keyRestrictionList = keyRestrictionList;
|
|
30
37
|
/** The addresses a key may be used from, which is the part of an authorized_keys line that
|
|
31
38
|
decides how much a stolen key is worth. A key with no restriction says so loudly. */
|
|
32
39
|
function keyRestriction(keyLine) {
|
|
33
|
-
let
|
|
34
|
-
if (!
|
|
35
|
-
return
|
|
40
|
+
let list = keyRestrictionList(keyLine);
|
|
41
|
+
if (!list) {
|
|
42
|
+
return exports.NO_RESTRICTION;
|
|
36
43
|
}
|
|
37
|
-
return
|
|
44
|
+
return list.join(",");
|
|
38
45
|
}
|
|
39
46
|
exports.keyRestriction = keyRestriction;
|
|
40
47
|
/** Enough to recognise whose key this is without printing the whole blob. */
|
|
@@ -50,6 +57,37 @@ function summarizeKey(keyLine) {
|
|
|
50
57
|
return `${type} ...${blob.slice(-12)}${comment && ` ${comment}` || ""}`;
|
|
51
58
|
}
|
|
52
59
|
exports.summarizeKey = summarizeKey;
|
|
60
|
+
/** What a set of keys has to satisfy before it is worth signing, as a list of complaints.
|
|
61
|
+
|
|
62
|
+
Every key needs a from=, because an unrestricted key can never be caught being used from the
|
|
63
|
+
wrong place, and being caught is the only thing that triggers a revocation.
|
|
64
|
+
|
|
65
|
+
No two keys may allow the same addresses, because that is one person holding two keys: revoking
|
|
66
|
+
one of them leaves the other working, so the revocation achieves nothing. */
|
|
67
|
+
function findKeyProblems(keys) {
|
|
68
|
+
let problems = [];
|
|
69
|
+
let byRestriction = new Map();
|
|
70
|
+
for (let key of keys) {
|
|
71
|
+
let restriction = keyRestrictionList(key);
|
|
72
|
+
if (!restriction) {
|
|
73
|
+
problems.push(`no from= restriction, so it can be used from anywhere:\n ${summarizeKey(key)}`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Sorted, so the same addresses written in a different order still count as the same.
|
|
77
|
+
let identity = [...restriction].sort().join(",");
|
|
78
|
+
byRestriction.set(identity, [...(byRestriction.get(identity) || []), key]);
|
|
79
|
+
}
|
|
80
|
+
for (let [restriction, sharing] of byRestriction) {
|
|
81
|
+
if (sharing.length < 2) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
problems.push(`${sharing.length} keys allow exactly the same addresses (${restriction}), which is one`
|
|
85
|
+
+ ` person holding more than one key:\n`
|
|
86
|
+
+ sharing.map(key => ` ${summarizeKey(key)}`).join("\n"));
|
|
87
|
+
}
|
|
88
|
+
return problems;
|
|
89
|
+
}
|
|
90
|
+
exports.findKeyProblems = findKeyProblems;
|
|
53
91
|
/** Reads the authorized keys a repo checkout wants applied. Prefers a top level authorized_keys
|
|
54
92
|
file and otherwise concatenates every .pub at the top level. */
|
|
55
93
|
async function readRepoKeys(repoPath) {
|
|
@@ -69,5 +107,5 @@ async function readRepoKeys(repoPath) {
|
|
|
69
107
|
return keys;
|
|
70
108
|
}
|
|
71
109
|
exports.readRepoKeys = readRepoKeys;
|
|
72
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aG9yaXplZEtleXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhdXRob3JpemVkS2V5cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSxvREFBNEI7QUFDNUIsMkRBQTZCO0FBQzdCLGdEQUF3QjtBQUV4Qiw2R0FBNkc7QUFDN0csK0ZBQStGO0FBQy9GLGdHQUFnRztBQUNoRyxTQUFTO0FBRVQsU0FBZ0IsYUFBYSxDQUFDLFFBQWdCO0lBQzFDLE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7QUFDdkcsQ0FBQztBQUZELHNDQUVDO0FBRUQ7OEVBQzhFO0FBQzlFLFNBQWdCLGNBQWMsQ0FBQyxPQUFlO0lBQzFDLElBQUksS0FBSyxHQUFHLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDeEMsSUFBSSxTQUFTLEdBQUcsS0FBSyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLG9CQUFvQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ3pFLElBQUksSUFBSSxHQUFHLFNBQVMsSUFBSSxDQUFDLElBQUksS0FBSyxDQUFDLFNBQVMsR0FBRyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDeEQsSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO1FBQ1IsT0FBTyxFQUFFLENBQUM7SUFDZCxDQUFDO0lBQ0QsT0FBTyxTQUFTLEdBQUcsZ0JBQU0sQ0FBQyxVQUFVLENBQUMsUUFBUSxDQUFDLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFF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cHRvIGZyb20gXCJjcnlwdG9cIjtcbmltcG9ydCBmcyBmcm9tIFwiZnMvcHJvbWlzZXNcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbi8vIFBPUlRFRCBDT0RFOiBzZWN1cml0eS9hdXRob3JpemVkS2V5cy9kYWVtb24vcG9ydHNlY3VyZURhZW1vbi5qcyBjb250YWlucyBhIHBsYWluIEpTIHBvcnQgb2Ygbm9ybWFsaXplS2V5cyxcbi8vIHN1bW1hcml6ZUtleSBhbmQgcmVhZFJlcG9LZXlzLCBzbyBpdCBjYW4gcmVzb2x2ZSB0aGUgc2FtZSBrZXlzIHdpdGggbm8gZGVwZW5kZW5jaWVzLiBUaGUgdHdvXG4vLyBtdXN0IGFncmVlIG9uIHdoaWNoIGtleXMgYSByZXBvIHByb2R1Y2VzIC0gaWYgeW91IGNoYW5nZSBvbmUsIG1ha2UgdGhlIG1hdGNoaW5nIGNoYW5nZSBpbiB0aGVcbi8vIG90aGVyLlxuXG5leHBvcnQgZnVuY3Rpb24gbm9ybWFsaXplS2V5cyhjb250ZW50czogc3RyaW5nKSB7XG4gICAgcmV0dXJuIGNvbnRlbnRzLnNwbGl0KFwiXFxuXCIpLm1hcChsaW5lID0+IGxpbmUudHJpbSgpKS5maWx0ZXIobGluZSA9PiBsaW5lICYmICFsaW5lLnN0YXJ0c1dpdGgoXCIjXCIpKTtcbn1cblxuLyoqIFRoZSBmaW5nZXJwcmludCBzc2ggaXRzZWxmIHJlcG9ydHMgZm9yIGEga2V5LCB3aGljaCBpcyB3aGF0IHRoZSBzc2hkIGxvZyBuYW1lcyBhbmQgdGhlcmVmb3JlXG4gICAgd2hhdCBhIHJldm9jYXRpb24gaXMga2V5ZWQgYnkuIFJldHVybnMgXCJcIiBmb3IgYSBsaW5lIHRoYXQgaG9sZHMgbm8ga2V5LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGtleUZpbmdlcnByaW50KGtleUxpbmU6IHN0cmluZykge1xuICAgIGxldCBwYXJ0cyA9IGtleUxpbmUudHJpbSgpLnNwbGl0KC9cXHMrLyk7XG4gICAgbGV0IHR5cGVJbmRleCA9IHBhcnRzLmZpbmRJbmRleChwYXJ0ID0+IC9eKHNzaC18ZWNkc2EtfHNrLSkvLnRlc3QocGFydCkpO1xuICAgIGxldCBibG9iID0gdHlwZUluZGV4ID49IDAgJiYgcGFydHNbdHlwZUluZGV4ICsgMV0gfHwgXCJcIjtcbiAgICBpZiAoIWJsb2IpIHtcbiAgICAgICAgcmV0dXJuIFwiXCI7XG4gICAgfVxuICAgIHJldHVybiBcIlNIQTI1NjpcIiArIGNyeXB0by5jcmVhdGVIYXNoKFwic2hhMjU2XCIpLnVwZGF0ZShCdWZmZXIuZnJvbShibG9iLCBcImJhc2U2NFwiKSkuZGlnZXN0KFwiYmFzZTY0XCIpLnJlcGxhY2UoLz0rJC8sIFwiXCIpO1xufVxuXG4vKiogVGhlIGFkZHJlc3NlcyBhIGtleSBtYXkgYmUgdXNlZCBmcm9tLCB3aGljaCBpcyB0aGUgcGFydCBvZiBhbiBhdXRob3JpemVkX2tleXMgbGluZSB0aGF0XG4gICAgZGVjaWRlcyBob3cgbXVjaCBhIHN0b2xlbiBrZXkgaXMgd29ydGguIEEga2V5IHdpdGggbm8gcmVzdHJpY3Rpb24gc2F5cyBzbyBsb3VkbHkuICovXG5leHBvcnQgZnVuY3Rpb24ga2V5UmVzdHJpY3Rpb24oa2V5TGluZTogc3RyaW5nKSB7XG4gICAgbGV0IG1hdGNoID0ga2V5TGluZS5tYXRjaCgvZnJvbT1cIihbXlwiXSopXCIvKTtcbiAgICBpZiAoIW1hdGNoKSB7XG4gICAgICAgIHJldHVybiBcIkFOWSBBRERSRVNTIChubyBmcm9tPSByZXN0cmljdGlvbilcIjtcbiAgICB9XG4gICAgcmV0dXJuIG1hdGNoWzFdO1xufVxuXG4vKiogRW5vdWdoIHRvIHJlY29nbmlzZSB3aG9zZSBrZXkgdGhpcyBpcyB3aXRob3V0IHByaW50aW5nIHRoZSB3aG9sZSBibG9iLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHN1bW1hcml6ZUtleShrZXlMaW5lOiBzdHJpbmcpIHtcbiAgICBsZXQgcGFydHMgPSBrZXlMaW5lLnRyaW0oKS5zcGxpdCgvXFxzKy8pO1xuICAgIGxldCB0eXBlSW5kZXggPSBwYXJ0cy5maW5kSW5kZXgocGFydCA9PiAvXihzc2gtfGVjZHNhLXxzay0pLy50ZXN0KHBhcnQpKTtcbiAgICBpZiAodHlwZUluZGV4IDwgMCkge1xuICAgICAgICByZXR1cm4ga2V5TGluZS5zbGljZSgwLCA2MCk7XG4gICAgfVxuICAgIGxldCB0eXBlID0gcGFydHNbdHlwZUluZGV4XTtcbiAgICBsZXQgYmxvYiA9IHBhcnRzW3R5cGVJbmRleCArIDFdIHx8IFwiXCI7XG4gICAgbGV0IGNvbW1lbnQgPSBwYXJ0cy5zbGljZSh0eXBlSW5kZXggKyAyKS5qb2luKFwiIFwiKTtcbiAgICByZXR1cm4gYCR7dHlwZX0gLi4uJHtibG9iLnNsaWNlKC0xMil9JHtjb21tZW50ICYmIGAgJHtjb21tZW50fWAgfHwgXCJcIn1gO1xufVxuXG4vKiogUmVhZHMgdGhlIGF1dGhvcml6ZWQga2V5cyBhIHJlcG8gY2hlY2tvdXQgd2FudHMgYXBwbGllZC4gUHJlZmVycyBhIHRvcCBsZXZlbCBhdXRob3JpemVkX2tleXNcbiAgICBmaWxlIGFuZCBvdGhlcndpc2UgY29uY2F0ZW5hdGVzIGV2ZXJ5IC5wdWIgYXQgdGhlIHRvcCBsZXZlbC4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZWFkUmVwb0tleXMocmVwb1BhdGg6IHN0cmluZykge1xuICAgIGxldCBjb21iaW5lZFBhdGggPSBwYXRoLmpvaW4ocmVwb1BhdGgsIFwiYXV0aG9yaXplZF9rZXlzXCIpO1xuICAgIGxldCBlbnRyaWVzID0gYXdhaXQgZnMucmVhZGRpcihyZXBvUGF0aCk7XG4gICAgaWYgKGVudHJpZXMuaW5jbHVkZXMoXCJhdXRob3JpemVkX2tleXNcIikpIHtcbiAgICAgICAgcmV0dXJuIG5vcm1hbGl6ZUtleXMoYXdhaXQgZnMucmVhZEZpbGUoY29tYmluZWRQYXRoLCBcInV0ZjhcIikpO1xuICAgIH1cbiAgICBsZXQgcHViRmlsZXMgPSBlbnRyaWVzLmZpbHRlcihuYW1lID0+IG5hbWUuZW5kc1dpdGgoXCIucHViXCIpKS5zb3J0KCk7XG4gICAgaWYgKCFwdWJGaWxlcy5sZW5ndGgpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBhdXRob3JpemVkX2tleXMgb3IgYXQgbGVhc3Qgb25lIC5wdWIgZmlsZSBpbiAke3JlcG9QYXRofSwgZm91bmQgbmVpdGhlcmApO1xuICAgIH1cbiAgICBsZXQga2V5czogc3RyaW5nW10gPSBbXTtcbiAgICBmb3IgKGxldCBuYW1lIG9mIHB1YkZpbGVzKSB7XG4gICAgICAgIGtleXMucHVzaCguLi5ub3JtYWxpemVLZXlzKGF3YWl0IGZzLnJlYWRGaWxlKHBhdGguam9pbihyZXBvUGF0aCwgbmFtZSksIFwidXRmOFwiKSkpO1xuICAgIH1cbiAgICByZXR1cm4ga2V5cztcbn1cbiJdfQ==
|
|
73
|
-
/* _JS_SOURCE_HASH = "
|
|
110
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aG9yaXplZEtleXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhdXRob3JpemVkS2V5cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSxvREFBNEI7QUFDNUIsMkRBQTZCO0FBQzdCLGdEQUF3QjtBQUV4QixTQUFnQixhQUFhLENBQUMsUUFBZ0I7SUFDMUMsT0FBTyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztBQUN2RyxDQUFDO0FBRkQsc0NBRUM7QUFFRDs4RUFDOEU7QUFDOUUsU0FBZ0IsY0FBYyxDQUFDLE9BQWU7SUFDMUMsSUFBSSxLQUFLLEdBQUcsT0FBTyxDQUFDLElBQUksRUFBRSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN4QyxJQUFJLFNBQVMsR0FBRyxLQUFLLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsb0JBQW9CLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7SUFDekUsSUFBSSxJQUFJLEdBQUcsU0FBUyxJQUFJLENBQUMsSUFBSSxLQUFLLENBQUMsU0FBUyxHQUFHLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUN4RCxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDUixPQUFPLEVBQUUsQ0FBQztJQUNkLENBQUM7SUFDRCxPQUFPLFNBQVMsR0FBRyxnQkFBTSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsT0FB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QUFDO1FBQ25CLE1BQU0sSUFBSSxLQUFLLENBQUMseURBQXlELFFBQVEsaUJBQWlCLENBQUMsQ0FBQztJQUN4RyxDQUFDO0lBQ0QsSUFBSSxJQUFJLEdBQWEsRUFBRSxDQUFDO0lBQ3hCLEtBQUssSUFBSSxJQUFJLElBQUksUUFBUSxFQUFFLENBQUM7UUFDeEIsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLGFBQWEsQ0FBQyxNQUFNLGtCQUFFLENBQUMsUUFBUSxDQUFDLGNBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxFQUFFLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN0RixDQUFDO0lBQ0QsT0FBTyxJQUFJLENBQUM7QUFDaEIsQ0FBQztBQWZELG9DQWVDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGNyeXB0byBmcm9tIFwiY3J5cHRvXCI7XG5pbXBvcnQgZnMgZnJvbSBcImZzL3Byb21pc2VzXCI7XG5pbXBvcnQgcGF0aCBmcm9tIFwicGF0aFwiO1xuXG5leHBvcnQgZnVuY3Rpb24gbm9ybWFsaXplS2V5cyhjb250ZW50czogc3RyaW5nKSB7XG4gICAgcmV0dXJuIGNvbnRlbnRzLnNwbGl0KFwiXFxuXCIpLm1hcChsaW5lID0+IGxpbmUudHJpbSgpKS5maWx0ZXIobGluZSA9PiBsaW5lICYmICFsaW5lLnN0YXJ0c1dpdGgoXCIjXCIpKTtcbn1cblxuLyoqIFRoZSBmaW5nZXJwcmludCBzc2ggaXRzZWxmIHJlcG9ydHMgZm9yIGEga2V5LCB3aGljaCBpcyB3aGF0IHRoZSBzc2hkIGxvZyBuYW1lcyBhbmQgdGhlcmVmb3JlXG4gICAgd2hhdCBhIHJldm9jYXRpb24gaXMga2V5ZWQgYnkuIFJldHVybnMgXCJcIiBmb3IgYSBsaW5lIHRoYXQgaG9sZHMgbm8ga2V5LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGtleUZpbmdlcnByaW50KGtleUxpbmU6IHN0cmluZykge1xuICAgIGxldCBwYXJ0cyA9IGtleUxpbmUudHJpbSgpLnNwbGl0KC9cXHMrLyk7XG4gICAgbGV0IHR5cGVJbmRleCA9IHBhcnRzLmZpbmRJbmRleChwYXJ0ID0+IC9eKHNzaC18ZWNkc2EtfHNrLSkvLnRlc3QocGFydCkpO1xuICAgIGxldCBibG9iID0gdHlwZUluZGV4ID49IDAgJiYgcGFydHNbdHlwZUluZGV4ICsgMV0gfHwgXCJcIjtcbiAgICBpZiAoIWJsb2IpIHtcbiAgICAgICAgcmV0dXJuIFwiXCI7XG4gICAgfVxuICAgIHJldHVybiBcIlNIQTI1NjpcIiArIGNyeXB0by5jcmVhdGVIYXNoKFwic2hhMjU2XCIpLnVwZGF0ZShCdWZmZXIuZnJvbShibG9iLCBcImJhc2U2NFwiKSkuZGlnZXN0KFwiYmFzZTY0XCIpLnJlcGxhY2UoLz0rJC8sIFwiXCIpO1xufVxuXG5leHBvcnQgY29uc3QgTk9fUkVTVFJJQ1RJT04gPSBcIkFOWSBBRERSRVNTIChubyBmcm9tPSByZXN0cmljdGlvbilcIjtcblxuLyoqIFRoZSBhZGRyZXNzZXMgYSBrZXkgbWF5IGJlIHVzZWQgZnJvbSwgb25lIGJ5IG9uZSwgb3IgdW5kZWZpbmVkIHdoZW4gdGhlIGtleSBjYXJyaWVzIG5vIGZyb209XG4gICAgYXQgYWxsLiBVbmRlZmluZWQgYW5kIGFuIGVtcHR5IGxpc3QgYXJlIHZlcnkgZGlmZmVyZW50IHRoaW5ncywgc28gdGhleSBzdGF5IGRpc3Rpbmd1aXNoYWJsZS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBrZXlSZXN0cmljdGlvbkxpc3Qoa2V5TGluZTogc3RyaW5nKSB7XG4gICAgbGV0IG1hdGNoID0ga2V5TGluZS5tYXRjaCgvZnJvbT1cIihbXlwiXSopXCIvKTtcbiAgICBpZiAoIW1hdGNoKSB7XG4gICAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gICAgfVxuICAgIHJldHVybiBtYXRjaFsxXS5zcGxpdChcIixcIikubWFwKGVudHJ5ID0+IGVudHJ5LnRyaW0oKSkuZmlsdGVyKGVudHJ5ID0+IGVudHJ5KTtcbn1cblxuLyoqIFRoZSBhZGRyZXNzZXMgYSBrZXkgbWF5IGJlIHVzZWQgZnJvbSwgd2hpY2ggaXMgdGhlIHBhcnQgb2YgYW4gYXV0aG9yaXplZF9rZXlzIGxpbmUgdGhhdFxuICAgIGRlY2lkZXMgaG93IG11Y2ggYSBzdG9sZW4ga2V5IGlzIHdvcnRoLiBBIGtleSB3aXRoIG5vIHJlc3RyaWN0aW9uIHNheXMgc28gbG91ZGx5LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGtleVJlc3RyaWN0aW9uKGtleUxpbmU6IHN0cmluZykge1xuICAgIGxldCBsaXN0ID0ga2V5UmVzdHJpY3Rpb25MaXN0KGtleUxpbmUpO1xuICAgIGlmICghbGlzdCkge1xuICAgICAgICByZXR1cm4gTk9fUkVTVFJJQ1RJT047XG4gICAgfVxuICAgIHJldHVybiBsaXN0LmpvaW4oXCIsXCIpO1xufVxuXG4vKiogRW5vdWdoIHRvIHJlY29nbmlzZSB3aG9zZSBrZXkgdGhpcyBpcyB3aXRob3V0IHByaW50aW5nIHRoZSB3aG9sZSBibG9iLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHN1bW1hcml6ZUtleShrZXlMaW5lOiBzdHJpbmcpIHtcbiAgICBsZXQgcGFydHMgPSBrZXlMaW5lLnRyaW0oKS5zcGxpdCgvXFxzKy8pO1xuICAgIGxldCB0eXBlSW5kZXggPSBwYXJ0cy5maW5kSW5kZXgocGFydCA9PiAvXihzc2gtfGVjZHNhLXxzay0pLy50ZXN0KHBhcnQpKTtcbiAgICBpZiAodHlwZUluZGV4IDwgMCkge1xuICAgICAgICByZXR1cm4ga2V5TGluZS5zbGljZSgwLCA2MCk7XG4gICAgfVxuICAgIGxldCB0eXBlID0gcGFydHNbdHlwZUluZGV4XTtcbiAgICBsZXQgYmxvYiA9IHBhcnRzW3R5cGVJbmRleCArIDFdIHx8IFwiXCI7XG4gICAgbGV0IGNvbW1lbnQgPSBwYXJ0cy5zbGljZSh0eXBlSW5kZXggKyAyKS5qb2luKFwiIFwiKTtcbiAgICByZXR1cm4gYCR7dHlwZX0gLi4uJHtibG9iLnNsaWNlKC0xMil9JHtjb21tZW50ICYmIGAgJHtjb21tZW50fWAgfHwgXCJcIn1gO1xufVxuXG4vKiogV2hhdCBhIHNldCBvZiBrZXlzIGhhcyB0byBzYXRpc2Z5IGJlZm9yZSBpdCBpcyB3b3J0aCBzaWduaW5nLCBhcyBhIGxpc3Qgb2YgY29tcGxhaW50cy5cblxuICAgIEV2ZXJ5IGtleSBuZWVkcyBhIGZyb209LCBiZWNhdXNlIGFuIHVucmVzdHJpY3RlZCBrZXkgY2FuIG5ldmVyIGJlIGNhdWdodCBiZWluZyB1c2VkIGZyb20gdGhlXG4gICAgd3JvbmcgcGxhY2UsIGFuZCBiZWluZyBjYXVnaHQgaXMgdGhlIG9ubHkgdGhpbmcgdGhhdCB0cmlnZ2VycyBhIHJldm9jYXRpb24uXG5cbiAgICBObyB0d28ga2V5cyBtYXkgYWxsb3cgdGhlIHNhbWUgYWRkcmVzc2VzLCBiZWNhdXNlIHRoYXQgaXMgb25lIHBlcnNvbiBob2xkaW5nIHR3byBrZXlzOiByZXZva2luZ1xuICAgIG9uZSBvZiB0aGVtIGxlYXZlcyB0aGUgb3RoZXIgd29ya2luZywgc28gdGhlIHJldm9jYXRpb24gYWNoaWV2ZXMgbm90aGluZy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBmaW5kS2V5UHJvYmxlbXMoa2V5czogc3RyaW5nW10pIHtcbiAgICBsZXQgcHJvYmxlbXM6IHN0cmluZ1tdID0gW107XG4gICAgbGV0IGJ5UmVzdHJpY3Rpb24gPSBuZXcgTWFwPHN0cmluZywgc3RyaW5nW10+KCk7XG4gICAgZm9yIChsZXQga2V5IG9mIGtleXMpIHtcbiAgICAgICAgbGV0IHJlc3RyaWN0aW9uID0ga2V5UmVzdHJpY3Rpb25MaXN0KGtleSk7XG4gICAgICAgIGlmICghcmVzdHJpY3Rpb24pIHtcbiAgICAgICAgICAgIHByb2JsZW1zLnB1c2goYG5vIGZyb209IHJlc3RyaWN0aW9uLCBzbyBpdCBjYW4gYmUgdXNlZCBmcm9tIGFueXdoZXJlOlxcbiAgICAgICR7c3VtbWFyaXplS2V5KGtleSl9YCk7XG4gICAgICAgICAgICBjb250aW51ZTtcbiAgICAgICAgfVxuICAgICAgICAvLyBTb3J0ZWQsIHNvIHRoZSBzYW1lIGFkZHJlc3NlcyB3cml0dGVuIGluIGEgZGlmZmVyZW50IG9yZGVyIHN0aWxsIGNvdW50IGFzIHRoZSBzYW1lLlxuICAgICAgICBsZXQgaWRlbnRpdHkgPSBbLi4ucmVzdHJpY3Rpb25dLnNvcnQoKS5qb2luKFwiLFwiKTtcbiAgICAgICAgYnlSZXN0cmljdGlvbi5zZXQoaWRlbnRpdHksIFsuLi4oYnlSZXN0cmljdGlvbi5nZXQoaWRlbnRpdHkpIHx8IFtdKSwga2V5XSk7XG4gICAgfVxuICAgIGZvciAobGV0IFtyZXN0cmljdGlvbiwgc2hhcmluZ10gb2YgYnlSZXN0cmljdGlvbikge1xuICAgICAgICBpZiAoc2hhcmluZy5sZW5ndGggPCAyKSB7XG4gICAgICAgICAgICBjb250aW51ZTtcbiAgICAgICAgfVxuICAgICAgICBwcm9ibGVtcy5wdXNoKFxuICAgICAgICAgICAgYCR7c2hhcmluZy5sZW5ndGh9IGtleXMgYWxsb3cgZXhhY3RseSB0aGUgc2FtZSBhZGRyZXNzZXMgKCR7cmVzdHJpY3Rpb259KSwgd2hpY2ggaXMgb25lYFxuICAgICAgICAgICAgKyBgIHBlcnNvbiBob2xkaW5nIG1vcmUgdGhhbiBvbmUga2V5OlxcbmBcbiAgICAgICAgICAgICsgc2hhcmluZy5tYXAoa2V5ID0+IGAgICAgICAke3N1bW1hcml6ZUtleShrZXkpfWApLmpvaW4oXCJcXG5cIilcbiAgICAgICAgKTtcbiAgICB9XG4gICAgcmV0dXJuIHByb2JsZW1zO1xufVxuXG4vKiogUmVhZHMgdGhlIGF1dGhvcml6ZWQga2V5cyBhIHJlcG8gY2hlY2tvdXQgd2FudHMgYXBwbGllZC4gUHJlZmVycyBhIHRvcCBsZXZlbCBhdXRob3JpemVkX2tleXNcbiAgICBmaWxlIGFuZCBvdGhlcndpc2UgY29uY2F0ZW5hdGVzIGV2ZXJ5IC5wdWIgYXQgdGhlIHRvcCBsZXZlbC4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZWFkUmVwb0tleXMocmVwb1BhdGg6IHN0cmluZykge1xuICAgIGxldCBjb21iaW5lZFBhdGggPSBwYXRoLmpvaW4ocmVwb1BhdGgsIFwiYXV0aG9yaXplZF9rZXlzXCIpO1xuICAgIGxldCBlbnRyaWVzID0gYXdhaXQgZnMucmVhZGRpcihyZXBvUGF0aCk7XG4gICAgaWYgKGVudHJpZXMuaW5jbHVkZXMoXCJhdXRob3JpemVkX2tleXNcIikpIHtcbiAgICAgICAgcmV0dXJuIG5vcm1hbGl6ZUtleXMoYXdhaXQgZnMucmVhZEZpbGUoY29tYmluZWRQYXRoLCBcInV0ZjhcIikpO1xuICAgIH1cbiAgICBsZXQgcHViRmlsZXMgPSBlbnRyaWVzLmZpbHRlcihuYW1lID0+IG5hbWUuZW5kc1dpdGgoXCIucHViXCIpKS5zb3J0KCk7XG4gICAgaWYgKCFwdWJGaWxlcy5sZW5ndGgpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBhdXRob3JpemVkX2tleXMgb3IgYXQgbGVhc3Qgb25lIC5wdWIgZmlsZSBpbiAke3JlcG9QYXRofSwgZm91bmQgbmVpdGhlcmApO1xuICAgIH1cbiAgICBsZXQga2V5czogc3RyaW5nW10gPSBbXTtcbiAgICBmb3IgKGxldCBuYW1lIG9mIHB1YkZpbGVzKSB7XG4gICAgICAgIGtleXMucHVzaCguLi5ub3JtYWxpemVLZXlzKGF3YWl0IGZzLnJlYWRGaWxlKHBhdGguam9pbihyZXBvUGF0aCwgbmFtZSksIFwidXRmOFwiKSkpO1xuICAgIH1cbiAgICByZXR1cm4ga2V5cztcbn1cbiJdfQ==
|
|
111
|
+
/* _JS_SOURCE_HASH = "a5147a2eb954468a9ddfcac36189a744f5b17f944d7e837ce0a525c780093a26"; */
|