Repository navigation
Expand file tree
/
Copy pathService.qml
More file actions
9411 lines (8642 loc) · 338 KB
/
Copy pathService.qml
File metadata and controls
9411 lines (8642 loc) · 338 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Pam
import qs.Commons
import "BitwardenModel.js" as Model
import "TotpModel.js" as Totp
// The vault, once per shell. The bar (and Panel.qml) exists once per monitor;
// the shell loads this `service` entry point once and hands it to every bar
// via `bar.shell.serviceFor()`, and each Panel.qml is a view of it. A view
// that cannot reach it creates a private one (Model.vaultHostDecision), so
// this must also work as a single view's own vault.
Item {
id: root
// Injected by the shell's service loader. A private host has neither.
property var shell: null
property var manifest: null
// Created by a view for itself because the shared service was unavailable.
property bool privateHost: false
// -------------------------------------------------------------------------
// Views
// -------------------------------------------------------------------------
//
// Attached bar copies, in attach order. A view detaches when destroyed (an
// unplugged monitor), leaving the vault running.
property var views: []
readonly property int viewCount: views.length
function attachView(view) {
if (!view || views.indexOf(view) !== -1) return
// Settings first: the first attach starts the vault, with these settings.
if (view.settings) updateSettings(view.settings)
views = views.concat([view])
}
// The view that acts when the vault needs the screen; see
// Model.presenterIndex().
readonly property string focusedScreen: Hyprland.focusedMonitor
? String(Hyprland.focusedMonitor.name || "") : ""
readonly property var presenter: {
var summaries = []
for (var i = 0; i < views.length; i++) {
summaries.push({ opened: views[i].opened === true, screen: views[i].screenName })
}
var index = Model.presenterIndex(summaries, focusedScreen)
return index >= 0 ? views[index] : nullPresenter
}
// Runs fn on every attached view.
function eachView(fn) {
var list = views.slice()
for (var i = 0; i < list.length; i++) fn(list[i])
}
// Whether any view's popout is open.
readonly property bool opened: {
for (var i = 0; i < views.length; i++) {
if (views[i].opened === true) return true
}
return false
}
// Live once a view is attached; see onLiveChanged.
readonly property bool live: viewCount > 0
property bool started: false
// Presenter stand-in when no view is attached, so late callbacks are safe.
QtObject {
id: nullPresenter
readonly property bool opened: false
readonly property string screenName: ""
function showPopout() {}
function hidePopout() {}
function focusField(name) {}
function fieldHasFocus(name) { return false }
function focusState() { return null }
function loginFieldHasFocus() { return false }
function unlockFieldHasFocus() { return false }
function syncLoginFields() {}
function syncSensitiveFields() {}
function revealListIndex(index) {}
function updateSettingsSticky() {}
}
function detachView(view) {
var index = views.indexOf(view)
if (index === -1) return
var next = views.slice()
next.splice(index, 1)
views = next
}
// -------------------------------------------------------------------------
// Settings
// -------------------------------------------------------------------------
//
// The bar entry's inline object in shell.json. A service only gets a load-time
// snapshot, so views (whose `settings` the bar keeps current) push them here;
// every view carries the same entry.
property var settings: ({})
function updateSettings(next) {
settings = next || ({})
}
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
// Settings, validated on the way in (nothing validates shell.json); see
// intSetting() in BitwardenModel.js.
readonly property int autoLockMinutes: Model.intSetting("autoLockMinutes", setting("autoLockMinutes"))
readonly property int clearClipboardSec: Model.intSetting("clearClipboardSec", setting("clearClipboardSec"))
readonly property bool lockOnScreenLock: Model.boolSetting("lockOnScreenLock", setting("lockOnScreenLock", true))
readonly property bool lockOnSuspend: Model.boolSetting("lockOnSuspend", setting("lockOnSuspend", true))
readonly property bool rememberSession: Model.boolSetting("rememberSession", setting("rememberSession", true))
readonly property int autoCopyTotpSec: Model.intSetting("autoCopyTotpSec", setting("autoCopyTotpSec"))
readonly property bool closeOnCopy: Model.boolSetting("closeOnCopy", setting("closeOnCopy", true))
readonly property bool colorizeIcon: Model.boolSetting("colorizeIcon", setting("colorizeIcon", false))
readonly property bool suggestOnOpen: Model.boolSetting("suggestOnOpen", setting("suggestOnOpen", true))
readonly property bool fingerprintUnlock: Model.boolSetting("fingerprintUnlock", setting("fingerprintUnlock", false))
readonly property bool fidoUnlock: Model.boolSetting("fidoUnlock", setting("fidoUnlock", false))
readonly property bool pinUnlock: Model.boolSetting("pinUnlock", setting("pinUnlock", false))
// Type the master password and PIN into pinentry, a separate process,
// rather than the panel. `pinentryProgram` (not in the settings screen)
// names a different program; unset, "" lets the scripts pick pinentry-qt
// when runnable, else `pinentry` from PATH.
readonly property bool usePinentry: Model.boolSetting("usePinentry", setting("usePinentry", true))
readonly property string pinentryProgramName: Model.pinentryProgram(setting("pinentryProgram", ""))
// Opt-in: nothing starts, binds or opens a FIFO while false.
readonly property bool sshAgentEnabled: Model.boolSetting("sshAgentEnabled", setting("sshAgentEnabled", false))
readonly property bool sshAgentUnlockOnDemand: Model.boolSetting("sshAgentUnlockOnDemand", setting("sshAgentUnlockOnDemand", false))
readonly property bool sshAgentApprovalPopup: Model.boolSetting("sshAgentApprovalPopup", setting("sshAgentApprovalPopup", true))
readonly property int sshAgentApprovalWindowSec: Model.intSetting("sshAgentApprovalWindowSec", setting("sshAgentApprovalWindowSec"))
// State
// status: "checking" | "unauthenticated" | "locked" | "unlocked"
property string status: "checking"
property string userEmail: ""
property string session: ""
property string masterPassword: ""
// Login form state
property string loginMethod: "email" // "email" | "apikey"
property string loginEmail: ""
property string loginPassword: ""
property string login2faCode: ""
property string loginServerRegion: "us" // "us" | "eu" | "custom"
property string loginServerUrl: ""
property string loginClientId: ""
property string loginClientSecret: ""
property bool show2faField: false
// Whether the running attempt carries --code; distinguishes a rejected code
// from new-device verification (loginNeedsDeviceVerification()).
property bool loginAttemptHadCode: false
// Bitwarden asked for new-device verification.
property bool loginDeviceVerification: false
// The two-step method passed to bw, or -1 to let bw decide (right when the
// account has one). See TWO_FACTOR_METHODS.
property int login2faMethod: rememberedTwoFactorMethod
// Picked in this login rather than remembered. A remembered method may be
// stale, so an unconfirmed one is dropped and retried without.
property bool login2faMethodConfirmed: false
property bool show2faMethodPicker: false
// New-device verification has its own code stage and login path; see
// deviceVerificationLoginCommand().
property string loginDeviceCode: ""
property bool showDeviceCodeField: false
// The one login with bw's prompts enabled is in flight.
property bool deviceVerificationAttempt: false
property bool deviceVerificationPending: false
// When the login began waiting on a second factor (epoch ms, 0 if not). A
// closed panel keeps it alive for SECOND_FACTOR_WINDOW_MS.
property double secondFactorStartedAt: 0
// This login's one automatic retry at delivering the password is spent.
property bool loginPasswordRetryUsed: false
// Email login stages: credentials, method picker, two-step code, device
// verification. One at a time.
readonly property bool loginCredentialsStage:
!show2faField && !show2faMethodPicker && !showDeviceCodeField
readonly property string login2faMethodLabel: Model.twoFactorMethodLabel(login2faMethod)
// The method the last attempt sent.
property int loginAttemptMethod: -1
// Per login email; follows loginEmail as it is typed.
readonly property var twoFactorMethodStore: setting("twoFactorMethods", null)
readonly property int rememberedTwoFactorMethod:
Model.rememberedTwoFactorMethodFor(twoFactorMethodStore, loginEmail)
// When the panel last launched a terminal login (epoch ms, 0 if never); a
// handoff is only read shortly after. See sessionHandoffReadCommand().
property double terminalLoginStartedAt: 0
// Navigation: "main" | "detail" | "edit" | "settings" | "setup" | "pin" |
// "fingerprint" | "generator" | "sends" | "sshApproval" | "locked". Lock and
// login screens follow `status`; "locked" only moves off the open screen.
property string currentScreen: "main"
property string screenBeforeSettings: "main"
// Dependency / setup state
property var dependencies: ({ items: [], hasOmarchy: true })
property bool depsChecked: false
property bool setupDismissed: false
// The last dependency probe's output, and `bw -v` cached by the binary's
// identity (dependencyBwId()) so opening the panel does not start Node.
property string depsRaw: ""
property bool bwVersionKnown: false
property string bwVersionId: ""
property string bwVersionValue: ""
property string listReadMode: "sanitized"
property var sshCapability: Model.defaultSshCapability()
// Show setup instead of probing `bw`; see setupGateActive().
readonly property bool setupGated: Model.setupGateActive(dependencies, depsChecked, setupDismissed)
// The first `bw status` was started; on a fresh install it waits for setup.
property bool statusProbeStarted: false
// A required tool was seen missing; cleared after the post-install probe.
property bool setupWasGated: false
property string settingsFlash: ""
property int settingsIndex: 0
readonly property var settingsEntries: Model.visibleSettings(dependencies, depsChecked)
// Vault data
property var items: []
// Reuse loaded data until stale (`bw list items` is slow); mutations always
// reload.
property double itemsLoadedAt: 0
property double orgsLoadedAt: 0
property double foldersLoadedAt: 0
readonly property int itemsFreshMs: 60000
// Organizations and folders outlive an item refresh many times over.
readonly property int metaFreshMs: 600000
property var filteredItems: []
property var organizations: []
property string selectedOrg: "all" // "all" | "personal" | orgId
property var folders: []
property string selectedFolder: "all" // "all" | "none" | folderId
// The open filter group: "" | "folders" | "organizations" | "types".
property string openFilterGroup: ""
property int filterOptionIndex: 0
readonly property int filterRowHeight: Style.space(30)
readonly property int filterVisibleRows: 5
readonly property var currentFilterOptions: openFilterGroup === "" ? [] : filterOptions(openFilterGroup)
readonly property int currentFilterVisibleRows: openFilterGroup === "types" ? currentFilterOptions.length : filterVisibleRows
// Added to the panel's height cap so the drawer opens downward.
readonly property int filterDrawerHeight: openFilterGroup === ""
? 0
: Style.space(30) + Math.min(currentFilterVisibleRows, currentFilterOptions.length) * filterRowHeight + Style.space(8)
property string formFolderId: ""
property string newFolderName: ""
// The expanded item-form picker: folder, organization, "customAdd",
// "customLabel:<row>" or "customLinked:<row>".
property string formPicker: ""
property var formCollections: []
property var formCollectionIds: []
property bool formCollectionsLoading: false
property bool creatingFolder: false
property string searchQuery: ""
property string selectedCategory: "all"
property int selectedIndex: 0
// Selected item detail
property var detailItem: null
property string detailPassword: ""
// Revealed sensitive fields of the open item: field key (as the helper names
// them: "password", "notes", "cardNumber", "cardCode", "ssn",
// "passportNumber", "licenseNumber", "customField:<index>", plus "totp",
// which has no value here) -> its value. Each eye is separate. A value is
// fetched when its eye is clicked and dropped when it is hidden, the item is
// closed or another opened, the screen is left or the vault locks.
property var revealedFields: ({})
function isFieldRevealed(key) { return revealedFields[key] !== undefined }
// What a revealed field shows; `listed` (the masked placeholder) otherwise.
function shownSecret(key, listed) {
var value = revealedFields[key]
return value !== undefined && value !== "" ? value : listed
}
// `value` undefined hides the field.
function setFieldRevealed(key, value) {
var next = {}
for (var k in revealedFields) next[k] = revealedFields[k]
if (value !== undefined) next[key] = String(value)
else delete next[key]
revealedFields = next
}
// Revealing a field of a re-prompt item asks for the master password
// first; hiding one never does.
function toggleFieldReveal(key) {
var item = detailItem
if (!item) return
if (isFieldRevealed(key)) {
setFieldRevealed(key, undefined)
// A re-prompt item's code is fetched when it is revealed only.
if (key === "totp" && itemNeedsReprompt(item)) liveTotp = ""
return
}
withReprompt(item, function() { root.revealField(item, key) })
}
function revealField(item, key) {
if (key === "totp") {
// The code is computed in the helper; there is no value to fetch.
setFieldRevealed("totp", "")
fetchTotp(String(item.id))
return
}
readDetailSecret(item, key, function(value) { root.setFieldRevealed(key, value) })
}
// Whether the detail shows (and keeps refreshing) its TOTP code: always,
// except that a re-prompt item's code waits for its eye.
function totpWanted(item) {
return !!item && (!itemNeedsReprompt(item) || isFieldRevealed("totp"))
}
// The list row of an item whose secrets the helper holds, else null.
function helperRow(id) {
if (!vaultHelperActive) return null
var row = Model.findItemById(items, String(id))
return row && row.secretsHeld ? row : null
}
// The open item's one secret `key`: from the helper when it holds the item,
// else from the detail, which then holds the value already.
// `callback(value)` runs only if that item's detail is still open.
function readDetailSecret(item, key, callback) {
var id = String(item.id)
if (!helperRow(id)) {
var local = Model.detailSecretValue(detailItem, key)
if (local !== null) callback(local)
return
}
var epoch = vaultEpoch
vaultQuery("field", { id: id, field: key }, function(ok, value) {
if (epoch !== root.vaultEpoch || root.currentScreen !== "detail"
|| !root.detailItem || String(root.detailItem.id) !== id || !root.repromptSatisfied(item)) return
if (!ok) {
root.errorMessage = "Could not read this value"
return
}
callback(String(value))
})
}
// The detail screen's copy of one secret: the helper hands it to wl-copy
// and it does not come here. Re-prompt items ask first.
function copyDetailField(key, label) {
var item = detailItem
if (!item) return
withReprompt(item, function() { root.copyDetailFieldNow(item, key, label) })
}
function copyDetailFieldNow(item, key, label) {
var id = String(item.id)
if (helperRow(id)) {
var epoch = vaultEpoch
vaultQuery("copyField", { id: id, field: key, clearSec: Math.max(0, Math.floor(Number(clearClipboardSec) || 0)) },
function(ok) {
if (epoch !== root.vaultEpoch) return
if (ok) {
root.resetAutoLockTimer()
root.flashNotification(label + " copied!")
} else {
root.errorMessage = "Could not read this value"
}
})
return
}
var value = Model.detailSecretValue(detailItem, key)
if (value) copyToClipboard(value, label)
}
// -------------------------------------------------------------------------
// Master password re-prompt
// -------------------------------------------------------------------------
//
// An item with `reprompt` 1 asks for the master password before anything
// secret of it is revealed, copied or edited, as Bitwarden's own clients
// do. withReprompt(item, callback) runs the
// callback at once for any other item, or once the password is confirmed:
// the Panel draws the prompt from repromptPending, repromptItemName,
// repromptError and repromptBusy, and answers with submitReprompt(password)
// or cancelReprompt(). The password is checked against the stored copy
// when quick unlock has one, else by `bw` itself (verifyMasterPassword());
// it travels in the environment, never argv, and is not kept. A success
// lasts only while that item's detail stays open: closing it, opening
// another item, closing the panel or locking asks again.
//
// With the vault helper the shell holds no item's secrets until a value is
// revealed or the item is edited: the detail is drawn from the list row
// (Model.publicItemDetail()). Revealing and copying go through the helper
// (readDetailSecret(), copyDetailField()), after the prompt for a re-prompt
// item; editing asks for the whole item (withRevealedDetail()). Ending the
// confirmation drops what was fetched.
property bool repromptPending: false
property string repromptItemId: ""
property string repromptItemName: ""
property string repromptError: ""
property bool repromptBusy: false
property var repromptCallback: null
property int repromptEpoch: -1
// The item whose open detail passed the re-prompt.
property string repromptVerifiedId: ""
// Set only while a just-confirmed action runs, so the gated functions it
// calls do not ask again.
property string repromptActionId: ""
function itemNeedsReprompt(item) {
return !!item && Number(item.reprompt) === 1
}
function repromptSatisfied(item) {
if (!itemNeedsReprompt(item)) return true
var id = String(item.id || "")
if (id === "") return false
if (id === repromptActionId) return true
return id === repromptVerifiedId && detailItem !== null && String(detailItem.id) === id
}
function withReprompt(item, callback) {
if (!item || typeof callback !== "function") return
if (repromptSatisfied(item)) {
callback()
return
}
if (status !== "unlocked") return
// A newer request replaces one still waiting.
repromptCallback = callback
repromptItemId = String(item.id || "")
repromptItemName = String(item.name || "this item")
repromptError = ""
repromptBusy = false
repromptEpoch = vaultEpoch
repromptPending = true
}
// `after(outcome)`, when given, is told "stale", "wrong" or "ok" once the
// password has been judged (before an accepted one's action runs).
function submitReprompt(password, after) {
if (!repromptPending || repromptBusy) return
var pw = String(password === undefined || password === null ? "" : password)
if (!pw) {
repromptError = "Enter your master password."
return
}
repromptBusy = true
repromptError = ""
var id = repromptItemId
var epoch = repromptEpoch
verifyMasterPassword(pw, function(ok) {
// Cancelled, replaced or locked meanwhile: nothing to run.
if (!root.repromptPending || root.repromptItemId !== id || root.repromptEpoch !== epoch
|| root.vaultEpoch !== epoch || root.status !== "unlocked") {
if (root.repromptItemId === id) root.repromptBusy = false
if (after) after("stale")
return
}
root.repromptBusy = false
if (!ok) {
root.repromptError = "That is not your master password."
if (after) after("wrong")
return
}
if (after) after("ok")
var callback = root.repromptCallback
root.repromptCallback = null
root.repromptPending = false
root.repromptError = ""
if (root.detailItem && String(root.detailItem.id) === id) root.repromptVerifiedId = id
root.repromptActionId = id
try {
if (callback) callback()
} finally {
root.repromptActionId = ""
}
})
pw = ""
}
function cancelReprompt() {
repromptPending = false
repromptCallback = null
repromptItemId = ""
repromptItemName = ""
repromptError = ""
repromptBusy = false
}
// Forgets a confirmed re-prompt and any prompt still waiting.
function clearRepromptGrant() {
repromptVerifiedId = ""
if (repromptPending) cancelReprompt()
withholdDetailSecrets()
}
// Drops every revealed value. An item the helper keeps goes back to its
// public view, with no password, notes or other secret that an edit loaded,
// and a re-prompt item's code is dropped too. A detail built from the list's
// own raw item (no helper) is otherwise left as it is.
function withholdDetailSecrets() {
revealedFields = ({})
if (!detailItem || !vaultHelperActive) return
if (itemNeedsReprompt(detailItem)) liveTotp = ""
if (detailItem.secretsWithheld || detailItem.typeCode === 5) return
var listed = Model.findItemById(items, String(detailItem.id))
if (listed && !listed.secretsHeld) return
detailItem = Model.publicItemDetail(detailItem)
detailPassword = ""
}
// withReprompt(), then the whole item in `detailItem`, then `callback`: the
// edit form's. For an item whose detail is already whole (no helper) it is
// withReprompt() alone. An answer for an item no longer open, a grant that
// ended, or a vault that locked meanwhile runs nothing.
function withRevealedDetail(item, callback) {
if (!item || typeof callback !== "function") return
withReprompt(item, function() { root.loadFullDetail(item, callback) })
}
function loadFullDetail(item, callback) {
var id = String(item.id || "")
if (!detailItem || String(detailItem.id) !== id) return
if (!detailItem.secretsWithheld) {
callback()
return
}
if (!vaultHelperActive) {
errorMessage = "Could not load item details"
return
}
var epoch = vaultEpoch
vaultQuery("item", { id: id }, function(ok, full) {
if (epoch !== root.vaultEpoch || root.currentScreen !== "detail"
|| !root.detailItem || String(root.detailItem.id) !== id || !root.repromptSatisfied(item)) return
if (root.detailItem.secretsWithheld) {
var parsed = ok ? Model.parseItemDetail(String(full)) : null
if (!parsed) {
root.errorMessage = "Could not load item details"
return
}
root.detailItem = parsed
root.detailPassword = parsed.password
}
callback()
})
}
// Whether `password` is the master password: `done(ok)`. The stored copy
// answers first when quick unlock has one that is current (a stale one
// holds an old password); anything but a match there, including a
// mismatch (the password may have changed elsewhere), is decided by `bw`,
// which mints and adopts a new session (verifyWithBw()).
function verifyMasterPassword(password, done) {
var pw = String(password || "")
if (!pw) { done(false); return }
if (!(quickUnlockAvailable && accountId && envelopeSummary && !envelopeSummary.stale)) {
verifyWithBw(pw, function(ok) { done(ok) })
return
}
var env = {}
env[Model.keyringSecretEnvVar()] = pw
queueEnvelopeJob({
command: Model.unlockEnvelopeCheckCommand(envelopeTool(), envelopeAccount()),
env: env,
onDone: function(code) {
if (code === 0) { done(true); return }
root.verifyWithBw(pw, function(ok) { done(ok) })
}
})
}
// The field `v` reveals: card number or password. None for identities.
readonly property string primaryRevealKey:
detailIsCard ? "cardNumber" : (detailIsLoginLike ? "password" : "")
// Which detail blocks the open item shows.
readonly property int detailTypeCode: detailItem ? Number(detailItem.typeCode || 1) : 1
readonly property bool detailIsLoginLike: detailTypeCode === 1 || detailTypeCode === 2
readonly property bool detailIsCard: detailTypeCode === 3
readonly property bool detailIsIdentity: detailTypeCode === 4
readonly property var detailCard: detailItem ? (detailItem.card || null) : null
readonly property var detailIdentity: detailItem ? (detailItem.identity || null) : null
// "MM / YY", or whichever half is set.
readonly property string detailCardExpiry: {
if (!detailCard) return ""
var m = String(detailCard.expMonth || "").trim()
var y = String(detailCard.expYear || "").trim()
if (m && y) return m + " / " + y
return m || y
}
readonly property string detailIdentityName: detailIdentity ? Model.identityFullName(detailIdentity) : ""
// Postal lines in envelope order, empty parts skipped.
readonly property string detailIdentityAddress: {
if (!detailIdentity) return ""
var id = detailIdentity
return Model.nonEmptyParts([id.address1, id.address2, id.address3,
Model.nonEmptyParts([id.city, id.state, id.postalCode]).join(" "), id.country]).join("\n")
}
property string liveTotp: ""
property int totpSecRemaining: 30
property string totpRequestItemId: ""
property string totpQueuedItemId: ""
property int totpQueuedEpoch: -1
property bool totpRestartPending: false
property string totpCopyItemId: ""
property string passwordCopyItemId: ""
// Attachment downloads run one at a time from a queue. `attachmentSaved`
// maps attachment id to saved path (for Open / Show in folder); cleared when
// another item opens.
property var attachmentQueue: []
property string attachmentBusyId: ""
// The in-flight attachment's file name, passed in the environment.
property string attachmentBusyName: ""
property var attachmentSaved: ({})
// Enter copies the password, then a second Enter copies the TOTP.
property var totpFollowupItem: null
property string totpFollowupCode: ""
property bool totpFollowupActive: false
// The save in flight, with the list and form before it, so a failure can
// restore both.
property var pendingSave: null
// The delete in flight, with the removed row for restoring on refusal.
property var pendingDelete: null
// A refused save's form, kept so it can be reopened rather than retyped.
property var failedSave: null
// Item form
property bool formIsEditing: false
property string formItemId: ""
property int formTypeCode: 1 // 1 login, 2 note, 3 card, 4 identity
property string formName: ""
property string formUsername: ""
property string formPassword: ""
property string formTotp: ""
property string formUri: ""
property string formNotes: ""
property bool formFavorite: false
property string formOrgId: ""
property bool formPasswordRevealed: false
property var formCustomFields: []
property int formNewCustomFieldType: 0
property string formNewCustomFieldName: ""
property string formCustomFieldLabelDraft: ""
property bool showDeleteConfirm: false
// Card and identity fields, flat like the rest of the form; formTypeFields()
// gathers them for the payload builders.
property string formCardholderName: ""
property string formCardBrand: ""
property string formCardNumber: ""
property string formCardExpMonth: ""
property string formCardExpYear: ""
property string formCardCode: ""
property string formIdTitle: ""
property string formIdFirstName: ""
property string formIdMiddleName: ""
property string formIdLastName: ""
property string formIdUsername: ""
property string formIdCompany: ""
property string formIdEmail: ""
property string formIdPhone: ""
property string formIdSsn: ""
property string formIdPassport: ""
property string formIdLicense: ""
property string formIdAddress1: ""
property string formIdAddress2: ""
property string formIdAddress3: ""
property string formIdCity: ""
property string formIdState: ""
property string formIdPostalCode: ""
property string formIdCountry: ""
// When the auto-lock window started (wall clock, so suspend counts).
property double autoLockArmedAt: 0
// The vault generation: advances on lock, logout and unlock. Readers record
// it so answers for a vault no longer open are dropped (vaultReadIsStale()).
property int vaultEpoch: 0
property var readEpochs: ({})
// Processes whose collectors still need emptying after a lock; see
// scrubSecretBuffers().
property var scrubPending: []
// Status & indicators
property bool isLoading: false
property bool isUnlocking: false
property bool isSyncing: false
property bool metadataLoadPending: false
property bool metadataForceRefresh: false
property bool statusRefreshAfterItems: false
property bool statusCheckAuthoritative: true
// This session already tried to repair an unsynced vault (onStatusFinished()).
property bool initialSyncAttempted: false
property bool syncReloadPending: false
property string errorMessage: ""
property string flashMessage: ""
property bool cursorActive: false
// Fingerprint unlock state.
property bool fingerprintAvailable: false // PAM stack + reader + enrolled finger
property bool fingerprintStored: false // a fingerprint way into the envelope, or the legacy entry
property bool fingerprintScanning: false
property bool fingerprintAuthorized: false // a live PAM success may consume one envelope open
property string fingerprintMessage: ""
// Why the last attempt failed; kept apart from fingerprintMessage (progress)
// so it is still shown on the PIN or password screen.
property string fingerprintError: ""
// FIDO2 unlock lives in FidoUnlock.qml; these forward what the lock screen
// and settings read.
readonly property bool fidoReady: fidoUnlocker.ready
readonly property bool fidoAvailable: fidoUnlocker.available
readonly property bool fidoStored: fidoUnlocker.stored
readonly property bool fidoScanning: fidoUnlocker.scanning
// Between a verified touch and the unlock it starts.
readonly property bool fidoAuthorized: fidoUnlocker.authorized
readonly property string fidoMessage: fidoUnlocker.message
// Edited by the setup screen; owned by FidoUnlock.qml.
property alias fidoSetupMaster: fidoUnlocker.setupMaster
// The setup form's error; fidoError is a failed touch.
property alias fidoSetupError: fidoUnlocker.error
property alias fidoBusy: fidoUnlocker.busy
readonly property string fidoError: fidoUnlocker.failure
property string pendingUnlockPassword: "" // held only until the unlock lands
// Auth processes start early and wait on a private FIFO; these tell that
// waiting apart from an attempt whose password was delivered.
property bool unlockSubmitted: false
property bool loginSubmitted: false
property bool loginSubmitAfterPrewarmStop: false
property bool loginPrepareAfterPrewarmStop: false
property string loginPrewarmSignature: ""
property string authPasswordWriteTarget: ""
property string authPasswordWriteValue: ""
// Item JSON for create/edit, passed in the environment.
property string itemPayloadJson: ""
property bool fpSetupActive: false
property string fpSetupMaster: ""
property string fpError: ""
property bool fpBusy: false
// What drove the in-flight unlock, so a stale stored secret is discarded
// rather than retried: "" | "fingerprint" | "fido" | "pin".
property string pendingUnlockFrom: ""
// Send state
property var sends: []
property bool sendsLoading: false
property string sendMode: "list" // "list" | "create"
property string sendPayloadJson: ""
property bool sendBusy: false
property string sendError: ""
property string sendFormName: ""
property string sendFormText: ""
property bool sendFormHidden: false
property int sendFormDays: 7
property int sendFormMaxAccess: 0
property string sendFormPassword: ""
property int sendIndex: 0
// Generator state (session-scoped; the browser extension's options)
property var genOpts: Model.generatorDefaults()
property string genValue: ""
property bool genBusy: false
property bool genRegeneratePending: false
property string genRequestSignature: ""
// `bw serve`: ready once it answers; failed means the CLI is used instead,
// because the server could not start or something else already answers on
// its socket (whose answers must not be trusted).
property bool generateServeReady: false
property bool generateServeStarting: false
property bool generateServeFailed: false
// We are stopping it, so its exit is not a bind failure.
property bool generateServeStopping: false
property bool generateCliStopping: false
property bool generateServeRequestStopping: false
property bool generateServeRequestPending: false
property var generateServeRequestPendingOptions: null
property var generateServeRequestPendingCallback: null
// Where Back/Esc go. Opened from the item form, the generator fills its
// password field and returns there.
property string generatorReturnScreen: "main"
readonly property bool generatorFeedsForm: generatorReturnScreen === "edit"
// PIN unlock state
property bool pinConfigured: false // a PIN way into the envelope, or the legacy blob
property string pinEntry: "" // locked-screen input
property int pinAttempts: 0
readonly property int pinMaxAttempts: 5
// The PIN setup form's error.
property string pinError: ""
// Why a PIN unlock failed, shown on whatever screen the user moves to.
property string pinUnlockError: ""
property string pinSetupPin: ""
property string pinSetupConfirm: ""
property string pinSetupMaster: ""
property bool pinBusy: false
property bool pinUnlockSubmitted: false
readonly property bool pinReady: pinUnlock && pinConfigured
// Valid but weak: turns the setup field red (pinWeakWarning()).
readonly property bool pinSetupWeak: Model.isPinWeak(pinSetupPin)
readonly property string userName: Quickshell.env("USER") || Quickshell.env("LOGNAME") || ""
// A closed lid puts the fingerprint reader out of reach (LidState.qml).
readonly property bool lidClosed: lidState.closed
// Cancel a scan when the lid closes; nothing else watches fingerprintReady.
onLidClosedChanged: if (lidClosed && fingerprintScanning) cancelFingerprintUnlock()
// Enrolled, stored and reachable. Everything that offers fingerprint unlock
// reads this.
readonly property bool fingerprintReady: fingerprintUnlock && fingerprintAvailable && fingerprintStored && !lidClosed
// Contextual suggestions state
property var activeWindowData: null
property var detectedContext: null
property var suggestedItems: []
property bool suggestionsDismissed: false
property var associations: Model.emptyAssociations()
property var learnedIds: ({})
property string pendingAssociationsJson: ""
property bool associationsWritePending: false
property bool associationsClearPending: false
property int associationsEpoch: 0
property int associationsReadEpoch: -1
property bool sessionStorePending: false
property bool sessionClearPending: false
// Which account's session the running store writes, and the accounts
// whose session clears wait behind a running one: a switch can land
// between a request and its run.
property string sessionStoreSlot: ""
property var sessionClearSlots: []
property bool pinClearPending: false
property bool masterClearPending: false
property bool allCredentialsClearPending: false
property bool logoutPending: false
property bool logoutCliDone: false
property bool logoutCredentialsDone: false
property int logoutExitCode: 0
property int logoutCredentialsExitCode: 0
readonly property bool logoutCleanupFailed: logoutPending && logoutCredentialsDone
&& logoutCredentialsExitCode !== 0
// Start with the first view. Until then the vault is inert: no `bw`, SSH
// agent or IPC target.
onLiveChanged: {
if (!root.live || root.started) return
root.started = true
// Dependencies first; the status probe follows in onDependenciesChecked,
// so a fresh install opens on setup rather than a doomed login form.
root.checkDependencies()
// Which account is active; learned suggestions and the status probe wait
// for it.
root.loadAccountRegistry()
// Also called explicitly: a binding already true at creation never fires
// its change handler.
root.syncSshAgentSupervision()
// Only changes after this are user transitions.
root.sshAgentSettingsReady = true
if (root.sshAgentEnabled) root.inspectSshAgentHelper()
root.inspectVaultHelper()
root.probePinentry()
root.inspectUnlockKey()
root.inspectQuickUnlockPrereqs()
root.inspectUwsmFragment()
}
readonly property var categories: [
{ id: "all", label: "All", icon: "" },
{ id: "login", label: "Logins", icon: "" },
{ id: "secureNote", label: "Notes", icon: "" },
{ id: "card", label: "Cards", icon: "" },
{ id: "identity", label: "Identities", icon: "" },
{ id: "sshKey", label: "SSH Keys", icon: "" },
{ id: "favorite", label: "Favorites", icon: "" }
]
// No SSH type filter until the probe confirms an SSH-capable CLI.
readonly property bool sshUiAvailable: Model.sshUiAvailable(dependencies, depsChecked)
readonly property var visibleCategories: sshUiAvailable
? categories
: categories.filter(function(category) { return category.id !== "sshKey" })
// -------------------------------------------------------------------------
// SSH companion supervision
// -------------------------------------------------------------------------
//
// Decisions live in Model.sshAgentReduce(); this side owns the Process, clock
// and timers. applySshAgentEvent() is the only place the state is replaced.
// Nothing here is on the path of ordinary vault operations: a failing helper
// only closes the signing gate.
// From this file's URL, so the helper runs by absolute path, not from PATH.
readonly property string sshAgentPluginDir: Model.pluginDirFromUrl(String(Qt.resolvedUrl(".")))
readonly property string sshAgentRuntimeDir: Quickshell.env("XDG_RUNTIME_DIR") || ""
// Inspected when enabled and whenever the plugin dir changes (an update can
// replace the binary under a running shell).
property var sshAgentHelper: Model.uninspectedHelper()
readonly property bool sshAgentSupervisable: sshAgentEnabled
&& sshAgentPluginDir !== "" && sshAgentRuntimeDir !== ""
// A helper that fails inspection disables this feature and nothing else:
// no supervisor, so no socket, no FIFO, and no agent branch in the vault
// read. The rest of the plugin never sees it.
&& Model.helperReady(sshAgentHelper)
function inspectSshAgentHelper() {
if (sshAgentHelperProc.running) return
sshAgentHelperProc.command = Model.sshAgentHelperInspectCommand(root.sshAgentPluginDir)
sshAgentHelperProc.running = true
}
function onSshAgentHelperInspected(raw) {
root.sshAgentHelper = Model.parseSshAgentHelperInspection(raw)
}
// The quick-unlock tool, inspected at every start: the envelope is written
// after each password login, before anyone opens settings. Failing disables
// only PIN, fingerprint and FIDO2 unlock.
property var unlockKeyHelper: Model.uninspectedHelper()
readonly property bool unlockKeyReady: Model.helperReady(unlockKeyHelper)
function inspectUnlockKey() {
if (unlockKeyProc.running || sshAgentPluginDir === "") return
unlockKeyProc.command = Model.unlockKeyInspectCommand(root.sshAgentPluginDir)
unlockKeyProc.running = true
}
function onUnlockKeyInspected(raw) {
root.unlockKeyHelper = Model.parseUnlockKeyInspection(raw)
root.envelopeReadinessChanged()
}
// -------------------------------------------------------------------------
// The vault helper
// -------------------------------------------------------------------------
//
// Holds the session key and the decrypted items outside this process, so
// a shell crash cannot put them in a core dump (docs/vault-helper.md). Every
// `bw` run is a VaultProcess: while the helper is up it runs there with the
// session added by the helper. Without one, runs wait and the vault stays
// locked; they run here, with the panel saying crash protection is off, only
// when `allowVaultWithoutHelper` is set (useVaultFallback()). Runs started
// before that is decided wait for it.
property var vaultHelper: Model.uninspectedHelper()
// Set as the shell unloads: a helper exit then is expected.
property bool shuttingDown: false
// "pending" | "starting" | "active" | "fallback" | "stopped"
property string vaultHelperState: "pending"
readonly property bool vaultHelperActive: vaultHelperState === "active"
property string vaultHelperWarning: ""
property int vaultRunSeq: 0
property var vaultRuns: ({})
property var vaultWaiting: []
property int vaultQuerySeq: 0
property var vaultQueries: ({})
// Restarts after an unexpected exit; past the limit, the helper is left
// stopped and the vault locked (stopVaultHelper()). The count clears once
// the helper has stayed up for vaultHelperSettledMs, so exits spread over a
// long session do not add up.
property int vaultHelperRestarts: 0
readonly property int vaultHelperMaxRestarts: 3
readonly property int vaultHelperSettledMs: 60000
readonly property int vaultHelperReadyMs: 10000
property bool vaultHelperReadyTimedOut: false
property bool vaultHelperRetryPending: false
// What `session` holds while the helper has the key (not a secret).
readonly property string heldSessionMarker: Model.vaultHeldSession()
// Held values by name while falling back (the helper holds them otherwise).
property var vaultLocalSecrets: ({})
// Only in shell.json, never in the settings screen, and only `true` counts.
readonly property bool allowVaultWithoutHelper: setting("allowVaultWithoutHelper", false) === true
function inspectVaultHelper() {
if (vaultHelperInspectProc.running) return
// The directory comes from this file's own URL and does not change, so
// waiting for it would leave every queued run waiting for good.
if (sshAgentPluginDir === "") {
useVaultFallback("the plugin's directory is not known, so the vault helper cannot be found.")
return
}
vaultHelperInspectProc.command = Model.vaultHelperInspectCommand(root.sshAgentPluginDir)
vaultHelperInspectProc.running = true
}