tabby-rdp 0.5.0 → 0.5.2
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/CHANGELOG.md +516 -0
- package/README.md +216 -58
- package/TROUBLESHOOTING.md +77 -0
- package/dist/accountForm.js +0 -1
- package/dist/accountRevisions.js +46 -0
- package/dist/accounts.js +2 -2
- package/dist/audio.js +0 -1
- package/dist/authorities.js +331 -0
- package/dist/byteQueue.js +102 -0
- package/dist/clipboard.js +106 -0
- package/dist/connectionStatus.js +0 -1
- package/dist/desk.js +16 -47
- package/dist/deskScript.js +196 -6
- package/dist/desktop.service.js +1499 -269
- package/dist/desktopForm.js +53 -18
- package/dist/desktops.js +232 -24
- package/dist/diskStorage.js +212 -0
- package/dist/drives.js +271 -30
- package/dist/fileTransfer.js +960 -132
- package/dist/gateway.js +216 -57
- package/dist/header.js +76 -5
- package/dist/help.js +16 -47
- package/dist/hostCompat.js +86 -0
- package/dist/hyperv.js +65 -15
- package/dist/index.js +7 -4
- package/dist/keyboard.js +0 -1
- package/dist/microphone.js +57 -13
- package/dist/ntlm.js +0 -1
- package/dist/osd.js +56 -15
- package/dist/packageChangeDialog.js +186 -0
- package/dist/probe.js +15 -3
- package/dist/rdcleanpath.js +319 -44
- package/dist/rdpFile.js +22 -7
- package/dist/rdpProfile.js +39 -21
- package/dist/releaseSettings.js +72 -0
- package/dist/releases.js +408 -0
- package/dist/remoteSetup.js +103 -23
- package/dist/sessionLog.js +53 -0
- package/dist/settingsPage.js +148 -122
- package/dist/signin.js +478 -48
- package/dist/ssh.js +99 -18
- package/dist/sshChannelStream.js +40 -6
- package/dist/targets.js +194 -40
- package/dist/ui.js +88 -20
- package/dist/unshowable.js +41 -0
- package/dist/updates.js +79 -56
- package/dist/vms.js +17 -3
- package/dist/wake.js +15 -8
- package/docs/desk.md +157 -0
- package/package.json +14 -4
- package/remote/trd-pty.py +74 -13
- package/vendor/SHA256SUMS +26 -0
- package/vendor/ironrdp_web_bg.wasm +0 -0
- package/dist/accountForm.js.map +0 -1
- package/dist/accounts.js.map +0 -1
- package/dist/audio.js.map +0 -1
- package/dist/connectionStatus.js.map +0 -1
- package/dist/desk.js.map +0 -1
- package/dist/deskScript.js.map +0 -1
- package/dist/desktop.service.js.map +0 -1
- package/dist/desktopForm.js.map +0 -1
- package/dist/desktops.js.map +0 -1
- package/dist/drives.js.map +0 -1
- package/dist/fileTransfer.js.map +0 -1
- package/dist/gateway.js.map +0 -1
- package/dist/header.js.map +0 -1
- package/dist/help.js.map +0 -1
- package/dist/hyperv.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keyboard.js.map +0 -1
- package/dist/microphone.js.map +0 -1
- package/dist/ntlm.js.map +0 -1
- package/dist/osd.js.map +0 -1
- package/dist/probe.js.map +0 -1
- package/dist/rdcleanpath.js.map +0 -1
- package/dist/rdpFile.js.map +0 -1
- package/dist/rdpProfile.js.map +0 -1
- package/dist/remoteSetup.js.map +0 -1
- package/dist/settingsPage.js.map +0 -1
- package/dist/signin.js.map +0 -1
- package/dist/ssh.js.map +0 -1
- package/dist/sshChannelStream.js.map +0 -1
- package/dist/targets.js.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/updates.js.map +0 -1
- package/dist/vms.js.map +0 -1
- package/dist/wake.js.map +0 -1
package/dist/signin.js
CHANGED
|
@@ -3,13 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.STYLE = exports.VAULT_SECRET_TYPE = void 0;
|
|
4
4
|
exports.useVault = useVault;
|
|
5
5
|
exports.storeName = storeName;
|
|
6
|
+
exports.forgetOnce = forgetOnce;
|
|
7
|
+
exports.renameCredentials = renameCredentials;
|
|
6
8
|
exports.loadCredentials = loadCredentials;
|
|
7
9
|
exports.hasCredentials = hasCredentials;
|
|
8
10
|
exports.saveCredentials = saveCredentials;
|
|
11
|
+
exports.saveCredentialsIf = saveCredentialsIf;
|
|
12
|
+
exports.forgetMark = forgetMark;
|
|
13
|
+
exports.forgottenSince = forgottenSince;
|
|
9
14
|
exports.forgetCredentials = forgetCredentials;
|
|
10
15
|
exports.forgetCredentialsOrFail = forgetCredentialsOrFail;
|
|
16
|
+
exports.forgetCredentialsIf = forgetCredentialsIf;
|
|
17
|
+
exports.gatewayScope = gatewayScope;
|
|
18
|
+
exports.isFor = isFor;
|
|
11
19
|
exports.forgetCredentialsFor = forgetCredentialsFor;
|
|
12
20
|
exports.moveCredentialsFor = moveCredentialsFor;
|
|
21
|
+
exports.forgetFormerKey = forgetFormerKey;
|
|
13
22
|
exports.askCredentials = askCredentials;
|
|
14
23
|
/**
|
|
15
24
|
* Sign-in for desktops that use their own accounts (Windows): a form in the desktop layer, and where passwords are
|
|
@@ -17,6 +26,7 @@ exports.askCredentials = askCredentials;
|
|
|
17
26
|
* keychain.
|
|
18
27
|
*/
|
|
19
28
|
const desktops_1 = require("./desktops");
|
|
29
|
+
const gateway_1 = require("./gateway");
|
|
20
30
|
const KEYCHAIN_SERVICE = 'tabby-rdp';
|
|
21
31
|
/** The Vault secret type of a desktop's or account's password; its key is `{ key, name }` (see vaultKey). */
|
|
22
32
|
exports.VAULT_SECRET_TYPE = 'rdp:password';
|
|
@@ -42,8 +52,14 @@ function storeName() {
|
|
|
42
52
|
* back, so two at once (two desktops signing in together) would lose one's change.
|
|
43
53
|
*/
|
|
44
54
|
let vaultQueue = Promise.resolve();
|
|
55
|
+
/** Same-origin windows share these locks. Store work holds the lock through its actual completion. */
|
|
56
|
+
async function storeLock(name, work, signal) {
|
|
57
|
+
const locks = globalThis.navigator?.locks;
|
|
58
|
+
return locks ? await locks.request(`tabby-rdp.credentials.${name}`, signal ? { signal } : {}, work) : work();
|
|
59
|
+
}
|
|
45
60
|
function queued(work) {
|
|
46
|
-
const
|
|
61
|
+
const run = () => storeLock('vault', work);
|
|
62
|
+
const next = vaultQueue.then(run, run);
|
|
47
63
|
vaultQueue = next.catch(() => null);
|
|
48
64
|
return next;
|
|
49
65
|
}
|
|
@@ -52,8 +68,9 @@ function vaultKey(key) {
|
|
|
52
68
|
return { key };
|
|
53
69
|
}
|
|
54
70
|
function vaultName(key) {
|
|
55
|
-
const
|
|
56
|
-
|
|
71
|
+
const { target, desktopId, gateway } = (0, desktops_1.keyParts)(key);
|
|
72
|
+
const where = `${desktopId}${gateway ? gateway.replace('@gateway#', ' through gateway ') : ''}`;
|
|
73
|
+
return target === 'account' ? `saved account` : target === desktops_1.DIRECT_KEY ? `${where} (direct)` : desktopId ? `${where} behind ${target}` : target;
|
|
57
74
|
}
|
|
58
75
|
// The system keychain can keep a call waiting indefinitely: on Linux, a locked or missing keyring waits for an unlock
|
|
59
76
|
// prompt, which a session without a keyring prompter never shows. Give up after a while. A call that never returns
|
|
@@ -81,16 +98,21 @@ function keytar() {
|
|
|
81
98
|
*/
|
|
82
99
|
function keychain(call) {
|
|
83
100
|
const run = () => new Promise((resolve, reject) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
resolve(undefined);
|
|
101
|
+
if (keychainStuck) {
|
|
102
|
+
reject(new Error('the keychain did not answer earlier'));
|
|
87
103
|
return;
|
|
88
104
|
}
|
|
105
|
+
const abort = new AbortController();
|
|
89
106
|
const timer = setTimeout(() => {
|
|
90
107
|
keychainStuck = true;
|
|
108
|
+
abort.abort(); // cancels a lock still waiting, never a native call already running
|
|
91
109
|
reject(new Error('the keychain did not answer'));
|
|
92
110
|
}, KEYCHAIN_TIMEOUT_MS);
|
|
93
|
-
|
|
111
|
+
// Timeout is outside the lock: a late native write keeps other windows' writes behind it until it ends.
|
|
112
|
+
storeLock('keychain', async () => {
|
|
113
|
+
const store = keytar();
|
|
114
|
+
return store ? call(store) : undefined;
|
|
115
|
+
}, abort.signal).then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
|
|
94
116
|
});
|
|
95
117
|
const next = keychainQueue.then(run, run);
|
|
96
118
|
keychainQueue = next.catch(() => null);
|
|
@@ -105,12 +127,198 @@ function parseCredentials(saved) {
|
|
|
105
127
|
return null;
|
|
106
128
|
}
|
|
107
129
|
}
|
|
130
|
+
let once = null;
|
|
131
|
+
let onceRunning = null;
|
|
132
|
+
/**
|
|
133
|
+
* Forgets the saved passwords whose key `forgets` matches, once in this computer's keychain and once in the Vault,
|
|
134
|
+
* before the store is next used: when it is asked anyway, so that a locked Vault asks for its passphrase at a sign-in
|
|
135
|
+
* rather than when Tabby starts. Each part's mark is read as that part is about to be done (another Tabby window may
|
|
136
|
+
* have done it since this one asked). A part that can't be done then (the keychain doesn't answer, or can't list what
|
|
137
|
+
* it holds; the Vault's prompt is cancelled) is tried again at the next use, and until it is done, what it would
|
|
138
|
+
* forget isn't looked up in that store (see withheld): the desktop asks.
|
|
139
|
+
*/
|
|
140
|
+
function forgetOnce(mark, forgets, vault) {
|
|
141
|
+
once = { mark, forgets, vault, keychainDone: false };
|
|
142
|
+
}
|
|
143
|
+
/** Does what forgetOnce and forgetLater left to do, if anything, and waits for it to end. */
|
|
144
|
+
async function used() {
|
|
145
|
+
if (once && !onceRunning) {
|
|
146
|
+
const job = once;
|
|
147
|
+
onceRunning = doOnce(job).catch(() => undefined).finally(() => { onceRunning = null; });
|
|
148
|
+
}
|
|
149
|
+
await onceRunning;
|
|
150
|
+
// A previous background pass may have failed before the store became available. This use gets one fresh attempt.
|
|
151
|
+
await laterRunning;
|
|
152
|
+
if (later.length && !laterRunning) {
|
|
153
|
+
laterRunning = doLater().catch(() => undefined).finally(() => { laterRunning = null; });
|
|
154
|
+
}
|
|
155
|
+
await laterRunning;
|
|
156
|
+
}
|
|
157
|
+
/** forgetOnce's work: the keychain's part first, and on its own, so a Vault that can't be read doesn't hold it up. */
|
|
158
|
+
async function doOnce(job) {
|
|
159
|
+
let forgets;
|
|
160
|
+
try {
|
|
161
|
+
forgets = job.forgets();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!job.keychainDone) {
|
|
167
|
+
try {
|
|
168
|
+
job.keychainDone = await keychainOnce(job.mark, forgets);
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
171
|
+
}
|
|
172
|
+
if (!job.vault.done()) {
|
|
173
|
+
try {
|
|
174
|
+
if (vaultOn()) {
|
|
175
|
+
// Read in full or not at all: a passphrase prompt cancelled throws, and the part is tried again.
|
|
176
|
+
const contents = await vault.load();
|
|
177
|
+
for (const secret of contents?.secrets ?? []) {
|
|
178
|
+
if (secret.type === exports.VAULT_SECRET_TYPE && typeof secret.key?.key === 'string' && forgets(secret.key.key)) {
|
|
179
|
+
await queued(() => vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(secret.key.key)));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// With the Vault off there is nothing in it: Tabby drops its contents when it is turned off.
|
|
184
|
+
job.vault.markDone();
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
}
|
|
188
|
+
if (job.keychainDone && job.vault.done() && once === job) {
|
|
189
|
+
once = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* This computer's keychain's part of forgetOnce: done before (its mark is there), or now, and marked. True without a
|
|
194
|
+
* keychain, where there is nothing to do. Throws when the keychain doesn't answer, or can't list what it holds: a
|
|
195
|
+
* part done on a listing that came back empty because of that would leave what it should have forgotten in place.
|
|
196
|
+
*/
|
|
197
|
+
async function keychainOnce(mark, forgets) {
|
|
198
|
+
const marked = await keychain(store => store.getPassword(KEYCHAIN_SERVICE, mark));
|
|
199
|
+
if (marked === undefined || marked) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
const entries = await keychain(store => store.findCredentials(KEYCHAIN_SERVICE));
|
|
203
|
+
for (const { account } of entries ?? []) {
|
|
204
|
+
if (typeof account === 'string' && account !== mark && forgets(account)) {
|
|
205
|
+
await keychain(store => store.deletePassword(KEYCHAIN_SERVICE, account));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await keychain(store => store.setPassword(KEYCHAIN_SERVICE, mark, 'done'));
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Forgets that couldn't be done when they were asked for (see forgetLater), each of one store's part: the Vault's
|
|
213
|
+
* (`inVault`), or the keychain's. Each is done the next time its store is used and can be read (see used, doLater),
|
|
214
|
+
* and what it would forget isn't looked up in that store meanwhile (see withheld). `saved`: keys a password was saved
|
|
215
|
+
* under in that store since, which is a new one, and stays. Kept in this window's memory, for as long as it is open.
|
|
216
|
+
*/
|
|
217
|
+
const later = [];
|
|
218
|
+
let laterRunning = null;
|
|
219
|
+
/**
|
|
220
|
+
* The saved passwords whose key `forgets` matches are to go from one store (the Vault, or with `inVault` false the
|
|
221
|
+
* keychain), and couldn't now (it couldn't be listed or wouldn't forget: a Vault whose passphrase prompt was
|
|
222
|
+
* cancelled, a keychain that didn't answer): forgotten once that store can be read (see doLater), and not used from it
|
|
223
|
+
* meanwhile, so that the desktop asks. The other store's part doesn't wait for it.
|
|
224
|
+
*/
|
|
225
|
+
function forgetLater(forgets, inVault) {
|
|
226
|
+
later.push({ forgets, inVault, saved: new Set() });
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* forgetLater's work, each store's part on its own, so that one that can't be read doesn't hold up the other: the
|
|
230
|
+
* keychain's whenever it can be listed; the Vault's at once while the Vault is off (there is nothing in it then: Tabby
|
|
231
|
+
* drops its contents when it is turned off), and otherwise once it is open. Not at the cost of a passphrase prompt of
|
|
232
|
+
* its own: with the Vault locked, its part waits for the Vault's next unlock (by whatever unlocks it, a password looked
|
|
233
|
+
* up or saved), and what it covers isn't used meanwhile.
|
|
234
|
+
*/
|
|
235
|
+
async function doLater() {
|
|
236
|
+
for (const inVault of [true, false]) {
|
|
237
|
+
const jobs = later.filter(job => job.inVault === inVault);
|
|
238
|
+
if (!jobs.length || inVault && vaultOn() && !vaultOpen()) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const entries = await entriesOf(inVault);
|
|
242
|
+
if (!entries) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (const job of jobs) {
|
|
246
|
+
if (await forgetListed(inVault, entries, job.forgets, job.saved)) {
|
|
247
|
+
later.splice(later.indexOf(job), 1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function vaultOpen() {
|
|
253
|
+
try {
|
|
254
|
+
return !!vault?.isOpen();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Whether `key` is one forgetOnce's work would forget from that store (the Vault, or with `inVault` false the
|
|
262
|
+
* keychain), and that part isn't done yet (see used), or one a forget left for later would forget from that store (see
|
|
263
|
+
* forgetLater): what is saved under it there isn't used meanwhile.
|
|
264
|
+
*/
|
|
265
|
+
function withheld(key, inVault) {
|
|
266
|
+
if (later.some(job => job.inVault === inVault && !job.saved.has(key) && job.forgets(key))) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
const job = once;
|
|
270
|
+
if (!job || (inVault ? job.vault.done() : job.keychainDone)) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
return job.forgets()(key);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Targets whose key changed (see RemoteTargets.keyChanged): the key each had up to 0.5.0, by the key it has now. A
|
|
282
|
+
* password saved under the former one moves to the new one the first time it is looked for there (see loadCredentials):
|
|
283
|
+
* when its store is asked anyway, so a locked Vault asks for its passphrase at a sign-in rather than when a tab opens.
|
|
284
|
+
*/
|
|
285
|
+
const formerKeys = new Map();
|
|
286
|
+
/** A target's key changed from `from` to `to` (see formerKeys). */
|
|
287
|
+
function renameCredentials(from, to) {
|
|
288
|
+
if (from !== to) {
|
|
289
|
+
formerKeys.set(to, from);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
108
292
|
/**
|
|
109
293
|
* Saved credentials for a desktop (keyed by its session key) or an account (`account#<id>`), or null. With the Vault
|
|
110
|
-
* on, the keychain is still read for what was saved before it was turned on.
|
|
294
|
+
* on, the keychain is still read for what was saved before it was turned on. Credentials saved under the target's
|
|
295
|
+
* former key (see formerKeys) are moved under this one, and returned.
|
|
111
296
|
*/
|
|
112
297
|
async function loadCredentials(key) {
|
|
113
|
-
|
|
298
|
+
await used();
|
|
299
|
+
const found = await loadSaved(key);
|
|
300
|
+
if (found) {
|
|
301
|
+
return found;
|
|
302
|
+
}
|
|
303
|
+
for (const [to, from] of formerKeys) {
|
|
304
|
+
const former = (0, desktops_1.rekeyed)(key, to, from);
|
|
305
|
+
if (former) {
|
|
306
|
+
const moved = await loadSaved(former);
|
|
307
|
+
if (moved) {
|
|
308
|
+
// Forgotten there only once saved here: should the store refuse, it is found there again next time.
|
|
309
|
+
try {
|
|
310
|
+
await saveCredentials(key, moved);
|
|
311
|
+
await forgetCredentialsOrFail(former);
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
}
|
|
315
|
+
return moved;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
async function loadSaved(key) {
|
|
321
|
+
if (vaultOn() && !withheld(key, true)) {
|
|
114
322
|
try {
|
|
115
323
|
const secret = await vault.getSecret(exports.VAULT_SECRET_TYPE, vaultKey(key));
|
|
116
324
|
const found = parseCredentials(secret?.value);
|
|
@@ -120,6 +328,9 @@ async function loadCredentials(key) {
|
|
|
120
328
|
}
|
|
121
329
|
catch { }
|
|
122
330
|
}
|
|
331
|
+
if (withheld(key, false)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
123
334
|
try {
|
|
124
335
|
return parseCredentials(await keychain(store => store.getPassword(KEYCHAIN_SERVICE, key)));
|
|
125
336
|
}
|
|
@@ -158,16 +369,97 @@ async function hasCredentials(key) {
|
|
|
158
369
|
}
|
|
159
370
|
}
|
|
160
371
|
async function saveCredentials(key, credentials, label) {
|
|
372
|
+
await saveCredentialsIf(key, credentials, () => true, label);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Checks a revision within the store queue/lock, including after an asynchronous write. If invalidated while writing,
|
|
376
|
+
* removes that write before a newer queued replacement can run. Never schedules a deletion behind a newer write.
|
|
377
|
+
*/
|
|
378
|
+
async function saveCredentialsIf(key, credentials, current, label) {
|
|
379
|
+
await used();
|
|
161
380
|
if (vaultOn()) {
|
|
162
|
-
await queued(() =>
|
|
381
|
+
const saved = await queued(async () => {
|
|
382
|
+
if (!current()) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
await vault.addSecret({ type: exports.VAULT_SECRET_TYPE, key: { ...vaultKey(key), description: label ?? vaultName(key) }, value: JSON.stringify(credentials) });
|
|
386
|
+
if (!current()) {
|
|
387
|
+
await vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key));
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
return true;
|
|
391
|
+
});
|
|
392
|
+
if (!saved) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
newlySaved(key, true);
|
|
163
396
|
// Not in two places: what the keychain had for it goes.
|
|
164
397
|
try {
|
|
165
|
-
await keychain(store =>
|
|
398
|
+
await keychain(async (store) => { if (current()) {
|
|
399
|
+
await store.deletePassword(KEYCHAIN_SERVICE, key);
|
|
400
|
+
} });
|
|
166
401
|
}
|
|
167
402
|
catch { }
|
|
168
|
-
return;
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
const saved = await keychain(async (store) => {
|
|
406
|
+
if (!current()) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
await store.setPassword(KEYCHAIN_SERVICE, key, JSON.stringify(credentials));
|
|
410
|
+
if (!current()) {
|
|
411
|
+
await store.deletePassword(KEYCHAIN_SERVICE, key);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
});
|
|
416
|
+
if (saved) {
|
|
417
|
+
newlySaved(key, false);
|
|
418
|
+
}
|
|
419
|
+
return !!saved;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* A password was saved under `key` just now, in the Vault or with `inVault` false the keychain, in place of what a
|
|
423
|
+
* forget left for later would forget from that store (see forgetLater): it stays. (What the other store has under that
|
|
424
|
+
* key is still the old one.) The rest of such a forget is done now, while the store that took the password can be read.
|
|
425
|
+
*/
|
|
426
|
+
function newlySaved(key, inVault) {
|
|
427
|
+
for (const job of later) {
|
|
428
|
+
if (job.inVault === inVault) {
|
|
429
|
+
job.saved.add(key);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (later.length) {
|
|
433
|
+
used().catch(() => undefined);
|
|
169
434
|
}
|
|
170
|
-
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* The latest forgets (a password forgotten for a key, an edit or a removal of a desktop, the passwords under a former
|
|
438
|
+
* key), each with a number and what it covers. A sign-in that began before one and ends after it, the server having
|
|
439
|
+
* taken its time, mustn't save its password again, which would undo the forget (see forgottenSince). Only the last
|
|
440
|
+
* FORGETS_KEPT are kept.
|
|
441
|
+
*/
|
|
442
|
+
const FORGETS_KEPT = 256;
|
|
443
|
+
const forgotten = [];
|
|
444
|
+
let forgetCount = 0;
|
|
445
|
+
/** Records a forget, as it is asked for: what began before it doesn't save under the keys it covers. */
|
|
446
|
+
function noteForget(covers) {
|
|
447
|
+
forgotten.push({ n: ++forgetCount, covers });
|
|
448
|
+
if (forgotten.length > FORGETS_KEPT) {
|
|
449
|
+
forgotten.shift();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** Where the forgets stand now: what a sign-in takes when its credentials are, to ask forgottenSince when it ends. */
|
|
453
|
+
function forgetMark() {
|
|
454
|
+
return forgetCount;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Whether a forget covering `key` was asked for since `mark` (see forgetMark): the desktop was edited or removed, or
|
|
458
|
+
* what was saved for it forgotten, while a sign-in ended later was under way. A mark older than the forgets kept counts
|
|
459
|
+
* as forgotten since: a sign-in that slow asks again, at worst.
|
|
460
|
+
*/
|
|
461
|
+
function forgottenSince(key, mark) {
|
|
462
|
+
return forgetCount - mark > forgotten.length || forgotten.some(f => f.n > mark && f.covers(key));
|
|
171
463
|
}
|
|
172
464
|
async function forgetCredentials(key) {
|
|
173
465
|
try {
|
|
@@ -177,56 +469,195 @@ async function forgetCredentials(key) {
|
|
|
177
469
|
}
|
|
178
470
|
/** As forgetCredentials, but a store that wouldn't (a Vault prompt cancelled, a keychain not answering) is reported. */
|
|
179
471
|
async function forgetCredentialsOrFail(key) {
|
|
472
|
+
noteForget(k => k === key);
|
|
473
|
+
await used();
|
|
474
|
+
return forgetNow(key);
|
|
475
|
+
}
|
|
476
|
+
/** An account edit/removal must not delete a replacement committed by a later edit. */
|
|
477
|
+
async function forgetCredentialsIf(key, current) {
|
|
478
|
+
noteForget(k => k === key);
|
|
479
|
+
await used();
|
|
180
480
|
if (vaultOn()) {
|
|
181
|
-
await queued(() =>
|
|
481
|
+
await queued(async () => { if (current()) {
|
|
482
|
+
await vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key));
|
|
483
|
+
} });
|
|
484
|
+
}
|
|
485
|
+
await keychain(async (store) => { if (current()) {
|
|
486
|
+
await store.deletePassword(KEYCHAIN_SERVICE, key);
|
|
487
|
+
} });
|
|
488
|
+
}
|
|
489
|
+
/** Forgets what is saved under `key`, in both stores. */
|
|
490
|
+
async function forgetNow(key) {
|
|
491
|
+
await forgetIn(true, key);
|
|
492
|
+
await forgetIn(false, key);
|
|
493
|
+
}
|
|
494
|
+
/** Forgets what is saved under `key` in one store: the Vault (while it is on), or with `inVault` false the keychain. */
|
|
495
|
+
async function forgetIn(inVault, key) {
|
|
496
|
+
if (inVault) {
|
|
497
|
+
if (vaultOn()) {
|
|
498
|
+
await queued(() => vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key)));
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
182
501
|
}
|
|
183
502
|
// No keychain at all is not a failure to forget; one that didn't answer is.
|
|
184
503
|
await keychain(store => store.deletePassword(KEYCHAIN_SERVICE, key));
|
|
185
504
|
}
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Every entry saved in one store, by its key: the Vault's (none while it is off: Tabby drops its contents when it is
|
|
507
|
+
* turned off), or with `inVault` false the keychain's (none without one). Null where that store can't be listed in full
|
|
508
|
+
* now (a Vault whose passphrase prompt was cancelled, a keychain that doesn't answer): what goes by such a list,
|
|
509
|
+
* forgetting or moving a desktop's passwords, would leave the rest in place, and in use. Each store is listed on its
|
|
510
|
+
* own, so that one that can't be doesn't keep what the other holds from being done.
|
|
511
|
+
*/
|
|
512
|
+
async function entriesOf(inVault) {
|
|
513
|
+
try {
|
|
514
|
+
if (!inVault) {
|
|
515
|
+
const entries = await keychain(store => store.findCredentials(KEYCHAIN_SERVICE));
|
|
516
|
+
return (entries ?? []).filter(entry => typeof entry?.account === 'string');
|
|
197
517
|
}
|
|
198
|
-
|
|
518
|
+
if (!vaultOn()) {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
const contents = await vault.load();
|
|
522
|
+
return (contents?.secrets ?? []).filter((secret) => secret?.type === exports.VAULT_SECRET_TYPE && typeof secret.key?.key === 'string')
|
|
523
|
+
.map((secret) => ({ account: secret.key.key, password: secret.value }));
|
|
199
524
|
}
|
|
200
|
-
|
|
201
|
-
|
|
525
|
+
catch {
|
|
526
|
+
return null;
|
|
202
527
|
}
|
|
203
|
-
catch { }
|
|
204
|
-
return entries;
|
|
205
528
|
}
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
529
|
+
/**
|
|
530
|
+
* Forgets from one store (the Vault, or with `inVault` false the keychain) what `entries`, all it holds (see
|
|
531
|
+
* entriesOf), has under a key `forgets` matches, but for those `kept`. False where it wouldn't forget one of them.
|
|
532
|
+
*/
|
|
533
|
+
async function forgetListed(inVault, entries, forgets, kept) {
|
|
534
|
+
let done = true;
|
|
535
|
+
for (const key of new Set(entries.map(({ account }) => account))) {
|
|
536
|
+
if (!kept?.has(key) && forgets(key)) {
|
|
537
|
+
try {
|
|
538
|
+
await forgetIn(inVault, key);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
done = false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return done;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* What a saved desktop password's key adds for the RD Gateway it goes through (`@gateway#host:port`, the gateway as
|
|
549
|
+
* parseGateway has it, so another spelling of it is the same one), or '' for none. Whenever a desktop has a gateway,
|
|
550
|
+
* its password is kept for that gateway too, a gateway account of its own or not: a password saved for one gateway
|
|
551
|
+
* (or for a connection without one) is never sent through a different gateway named for the same address, which could
|
|
552
|
+
* lead somewhere else, without asking.
|
|
553
|
+
*/
|
|
554
|
+
function gatewayScope(gateway) {
|
|
555
|
+
const parsed = gateway ? (0, gateway_1.parseGateway)(gateway) : null;
|
|
556
|
+
return parsed ? `@gateway#${parsed.host}:${parsed.port}` : '';
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Whether a credential key is one of a desktop's saved passwords (see SavedFor). Anchored on the key's parts, so an
|
|
560
|
+
* unrelated key that merely ends in `#<id>` (a gateway pin, or a desktop whose own id is a suffix of another's) isn't
|
|
561
|
+
* matched; a gateway suffix on the key doesn't change the id.
|
|
562
|
+
*/
|
|
563
|
+
function isFor(account, desktopId, which) {
|
|
564
|
+
const { target, desktopId: id, gateway } = (0, desktops_1.keyParts)(account);
|
|
565
|
+
return id === desktopId && (which.direct ? target === desktops_1.DIRECT_KEY : target !== desktops_1.DIRECT_KEY && target !== 'gateway' && target !== 'account') &&
|
|
566
|
+
(which.scope === undefined || gateway === which.scope);
|
|
209
567
|
}
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Forgets a desktop's saved passwords (see SavedFor; by default those behind any SSH host, for any gateway), from each
|
|
570
|
+
* store on its own. Where one can't be listed or won't forget now, its part goes later, and what it has of them isn't
|
|
571
|
+
* used meanwhile (see forgetLater): a user name changed while the Vault's prompt is cancelled doesn't leave the old
|
|
572
|
+
* one's password to sign in with, and a keychain that doesn't answer doesn't keep the Vault's from going at once.
|
|
573
|
+
* Returns the stores whose part waits (none, as a rule).
|
|
574
|
+
*/
|
|
575
|
+
async function forgetCredentialsFor(desktopId, which = {}) {
|
|
576
|
+
const forgets = (key) => isFor(key, desktopId, which);
|
|
577
|
+
noteForget(forgets);
|
|
578
|
+
await used();
|
|
579
|
+
const waiting = [];
|
|
580
|
+
for (const inVault of [true, false]) {
|
|
581
|
+
const entries = await entriesOf(inVault);
|
|
582
|
+
if (!entries || !await forgetListed(inVault, entries, forgets)) {
|
|
583
|
+
forgetLater(forgets, inVault);
|
|
584
|
+
waiting.push(inVault ? 'vault' : 'keychain');
|
|
215
585
|
}
|
|
216
586
|
}
|
|
587
|
+
return waiting;
|
|
217
588
|
}
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
589
|
+
/**
|
|
590
|
+
* A desktop's address changed: its saved passwords (see SavedFor) move to the new address, for the same gateway. One
|
|
591
|
+
* that another desktop already has there (one at that address behind the same host, say) stays that desktop's: the
|
|
592
|
+
* moved one is dropped rather than put in its place, and the edited desktop asks. So is one for an address where a
|
|
593
|
+
* desktop is known already (`known`, by the session key there: a certificate is remembered for it, as it is when each
|
|
594
|
+
* password is looked at): the machine there may be another desktop's, which would otherwise get the edited desktop's
|
|
595
|
+
* password unasked, its certificate checking out as its own.
|
|
596
|
+
*
|
|
597
|
+
* Behind an SSH host, a password moves only with the certificate remembered for its desktop (`known` at the old
|
|
598
|
+
* address), which goes along and stays at the old address too (see RemoteDesktopService.desktopEdited): a desktop
|
|
599
|
+
* behind a host trusts its first certificate unasked, so a password that came to the new address alone would go to
|
|
600
|
+
* whatever answers there first, and so would one that stayed at the old address without it (another desktop's,
|
|
601
|
+
* reached another way). The keys don't show which host a desktop entry is behind, so an edit moves the passwords at
|
|
602
|
+
* that address behind every host: this way none of them reaches a machine at the new address that isn't the one it was
|
|
603
|
+
* saved for (another host's, or the one a mistyped address leads to) without a question. Without a certificate, a
|
|
604
|
+
* password is dropped, and that desktop asks. What isn't to be used (see withheld) doesn't move either. Each store's
|
|
605
|
+
* part is done on its own: one that can't be listed (a Vault whose prompt was cancelled, a keychain that doesn't
|
|
606
|
+
* answer) moves nothing, what it has at the old address goes later (see forgetLater), and what it has at the new one
|
|
607
|
+
* can't be seen, so only the other store's tells what is there already. Returns the stores whose part waits (none, as
|
|
608
|
+
* a rule): what they have at the old address is found there again should this window close first, where the
|
|
609
|
+
* certificate stays (see RemoteDesktopService.desktopEdited).
|
|
610
|
+
*/
|
|
611
|
+
async function moveCredentialsFor(fromId, toId, which = {}, known = () => false) {
|
|
612
|
+
const forgets = (key) => isFor(key, fromId, which);
|
|
613
|
+
noteForget(forgets);
|
|
614
|
+
await used();
|
|
615
|
+
const stores = [];
|
|
616
|
+
for (const inVault of [true, false]) {
|
|
617
|
+
stores.push({ inVault, entries: await entriesOf(inVault) });
|
|
618
|
+
}
|
|
619
|
+
const taken = new Set(stores.flatMap(({ entries }) => entries ?? []).map(({ account }) => account));
|
|
620
|
+
const waiting = [];
|
|
621
|
+
for (const { inVault, entries } of stores) {
|
|
622
|
+
let done = !!entries;
|
|
623
|
+
for (const { account, password } of entries ?? []) {
|
|
624
|
+
const credentials = parseCredentials(password);
|
|
625
|
+
const { target, gateway } = (0, desktops_1.keyParts)(account);
|
|
626
|
+
if (forgets(account) && credentials) {
|
|
627
|
+
const to = `${target}#${toId}${gateway}`;
|
|
628
|
+
const pinned = target === desktops_1.DIRECT_KEY || known(`${target}#${fromId}`);
|
|
629
|
+
try {
|
|
630
|
+
if (!taken.has(to) && !known(`${target}#${toId}`) && pinned && !withheld(account, inVault)) {
|
|
631
|
+
await saveCredentials(to, credentials);
|
|
632
|
+
taken.add(to);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch { }
|
|
636
|
+
try {
|
|
637
|
+
await forgetIn(inVault, account);
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
done = false;
|
|
641
|
+
}
|
|
226
642
|
}
|
|
227
|
-
|
|
643
|
+
}
|
|
644
|
+
if (!done) {
|
|
645
|
+
forgetLater(forgets, inVault);
|
|
646
|
+
waiting.push(inVault ? 'vault' : 'keychain');
|
|
228
647
|
}
|
|
229
648
|
}
|
|
649
|
+
return waiting;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* A target's key changed from `from`, a key no target has any more, whose saved passwords nothing moves (see
|
|
653
|
+
* RemoteTargets.keyChanged): those under it (its own desktop's and those of the desktops behind it, see rekeyed) are
|
|
654
|
+
* forgotten from each store once it can be read, and not used from it meanwhile (see forgetLater).
|
|
655
|
+
*/
|
|
656
|
+
function forgetFormerKey(from) {
|
|
657
|
+
const forgets = (key) => (0, desktops_1.rekeyed)(key, from, from) !== null;
|
|
658
|
+
noteForget(forgets);
|
|
659
|
+
forgetLater(forgets, true);
|
|
660
|
+
forgetLater(forgets, false);
|
|
230
661
|
}
|
|
231
662
|
exports.STYLE = `
|
|
232
663
|
.trd-signin { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 1; }
|
|
@@ -297,4 +728,3 @@ function askCredentials(layer, options, abort) {
|
|
|
297
728
|
abort.then(() => done(null));
|
|
298
729
|
});
|
|
299
730
|
}
|
|
300
|
-
//# sourceMappingURL=signin.js.map
|