Skip to content

Commit 1643659

Browse files
committed
improve wizard UI and expand the search
Signed-off-by: Pau Escrich <p4u@dabax.net>
1 parent 3116e7d commit 1643659

9 files changed

Lines changed: 1354 additions & 318 deletions

File tree

internal/app/app.go

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package app
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"log"
78
"os"
@@ -44,6 +45,7 @@ type App struct {
4445
// State
4546
Identities []pkcs12store.Identity
4647
SystemIdentities []pkcs12store.Identity
48+
LockedP12 []string
4749

4850
// Current Action State
4951
CurrentReq *model.SignRequest
@@ -100,6 +102,14 @@ func (a *App) IdentitiesSnapshot() []pkcs12store.Identity {
100102
return out
101103
}
102104

105+
func (a *App) LockedP12Snapshot() []string {
106+
a.mu.RLock()
107+
defer a.mu.RUnlock()
108+
out := make([]string, len(a.LockedP12))
109+
copy(out, a.LockedP12)
110+
return out
111+
}
112+
103113
func (a *App) SetIdentities(ids []pkcs12store.Identity) {
104114
a.mu.Lock()
105115
defer a.mu.Unlock()
@@ -188,6 +198,8 @@ func (a *App) runUpdateCheck(force bool) {
188198
}
189199

190200
func (a *App) ScanSystemStores(ctx context.Context) {
201+
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
202+
defer cancel()
191203
start := time.Now()
192204
log.Printf("DEBUG: ScanSystemStores started")
193205
var all []pkcs12store.Identity
@@ -204,21 +216,55 @@ func (a *App) ScanSystemStores(ctx context.Context) {
204216
}
205217

206218
// 2. NSS Stores
207-
nssStores := systemstore.DiscoverNSSStores()
219+
nssStores := systemstore.DiscoverNSSStores(ctx)
208220
log.Printf("DEBUG: ScanSystemStores: discovered %d NSS stores", len(nssStores))
221+
var nssMu sync.Mutex
222+
sem := make(chan struct{}, 4)
223+
var wg sync.WaitGroup
209224
for _, s := range nssStores {
210-
log.Printf("DEBUG: ScanSystemStores: scanning NSS store label=%q profile=%q", s.Label, s.ProfileDir)
211-
ids, err := safeList(s.List, ctx, "NSS store "+s.Label)
212-
if err == nil {
213-
all = append(all, ids...)
214-
log.Printf("DEBUG: ScanSystemStores: NSS store %q returned %d identities", s.Label, len(ids))
215-
} else {
216-
log.Printf("DEBUG: ScanSystemStores: NSS store %q error: %v", s.Label, err)
225+
s := s
226+
sem <- struct{}{}
227+
wg.Add(1)
228+
go func() {
229+
defer func() {
230+
<-sem
231+
wg.Done()
232+
}()
233+
log.Printf("DEBUG: ScanSystemStores: scanning NSS store label=%q profile=%q", s.Label, s.ProfileDir)
234+
ids, err := safeList(s.List, ctx, "NSS store "+s.Label)
235+
if err == nil {
236+
nssMu.Lock()
237+
all = append(all, ids...)
238+
nssMu.Unlock()
239+
log.Printf("DEBUG: ScanSystemStores: NSS store %q returned %d identities", s.Label, len(ids))
240+
} else {
241+
log.Printf("DEBUG: ScanSystemStores: NSS store %q error: %v", s.Label, err)
242+
}
243+
}()
244+
}
245+
wg.Wait()
246+
247+
// 3. PKCS#12 files (passwordless only)
248+
var lockedP12 []string
249+
p12Paths := systemstore.FindPKCS12Candidates(ctx, 5, 200)
250+
log.Printf("DEBUG: ScanSystemStores: discovered %d candidate PKCS#12 files", len(p12Paths))
251+
for _, p := range p12Paths {
252+
id, err := systemstore.ParsePKCS12Metadata(p, "")
253+
if err != nil {
254+
if errors.Is(err, systemstore.ErrPKCS12PasswordRequired) {
255+
log.Printf("DEBUG: PKCS#12 file requires password, skipping auto-import: %s", p)
256+
lockedP12 = append(lockedP12, p)
257+
} else {
258+
log.Printf("DEBUG: PKCS#12 parse skipped for %s: %v", p, err)
259+
}
260+
continue
217261
}
262+
all = append(all, id)
218263
}
219264

220265
a.mu.Lock()
221266
defer a.mu.Unlock()
267+
a.LockedP12 = lockedP12
222268

223269
// Deduplicate based on Fingerprint
224270
seen := make(map[string]bool)

internal/crypto/systemstore/nss.go

Lines changed: 157 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type nssIdentityDTO struct {
3939
IDHex string `json:"idHex"`
4040
}
4141

42-
func DiscoverNSSStores() []*NSSStore {
42+
func DiscoverNSSStores(ctx context.Context) []*NSSStore {
4343
var stores []*NSSStore
4444
seen := make(map[string]struct{})
4545

@@ -49,11 +49,25 @@ func DiscoverNSSStores() []*NSSStore {
4949
}
5050

5151
addStore := func(profileDir, label string) {
52+
select {
53+
case <-ctx.Done():
54+
return
55+
default:
56+
}
5257
if profileDir == "" {
5358
return
5459
}
5560
profileDir = filepath.Clean(profileDir)
56-
if _, err := os.Stat(filepath.Join(profileDir, "cert9.db")); err != nil {
61+
// Accept both modern cert9.db and legacy cert8.db
62+
hasCert9 := func() bool {
63+
_, err := os.Stat(filepath.Join(profileDir, "cert9.db"))
64+
return err == nil
65+
}()
66+
hasCert8 := func() bool {
67+
_, err := os.Stat(filepath.Join(profileDir, "cert8.db"))
68+
return err == nil
69+
}()
70+
if !hasCert9 && !hasCert8 {
5771
return
5872
}
5973
if _, ok := seen[profileDir]; ok {
@@ -82,44 +96,57 @@ func DiscoverNSSStores() []*NSSStore {
8296
addStore(profileDir, label)
8397
}
8498

85-
// 3. Chromium/Brave NSS DBs (mostly Linux, with optional profile paths on other OSes).
86-
var chromiumBases []string
87-
switch runtime.GOOS {
88-
case "windows":
89-
localAppData := localAppDataDir()
90-
chromiumBases = []string{
91-
filepath.Join(localAppData, "Google", "Chrome", "User Data"),
92-
filepath.Join(localAppData, "BraveSoftware", "Brave-Browser", "User Data"),
93-
filepath.Join(localAppData, "Chromium", "User Data"),
94-
}
95-
case "darwin":
96-
chromiumBases = []string{
97-
filepath.Join(home, "Library", "Application Support", "Google", "Chrome"),
98-
filepath.Join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
99-
filepath.Join(home, "Library", "Application Support", "Chromium"),
100-
}
101-
default:
102-
chromiumBases = []string{
103-
filepath.Join(home, ".config", "google-chrome"),
104-
filepath.Join(home, ".config", "BraveSoftware", "Brave-Browser"),
105-
filepath.Join(home, ".config", "chromium"),
106-
filepath.Join(home, "snap", "brave", "common", ".pki", "nssdb"),
107-
}
108-
snapPaths, _ := filepath.Glob(filepath.Join(home, "snap", "*", "current", ".pki", "nssdb"))
109-
chromiumBases = append(chromiumBases, snapPaths...)
110-
}
111-
for _, base := range chromiumBases {
99+
// 3. Chromium-family NSS DBs — covers Chrome, Brave, Edge, Opera, Vivaldi, etc.
100+
for _, base := range chromiumBaseDirs() {
112101
addStore(base, "Browser NSS")
113-
114102
entries, _ := os.ReadDir(base)
115103
for _, entry := range entries {
116-
if entry.IsDir() && (entry.Name() == "Default" || strings.HasPrefix(entry.Name(), "Profile ")) {
117-
profileDir := filepath.Join(base, entry.Name())
118-
addStore(profileDir, "Browser Profile: "+entry.Name())
104+
if !entry.IsDir() {
105+
continue
106+
}
107+
n := entry.Name()
108+
if n == "Default" || strings.HasPrefix(n, "Profile ") || strings.HasPrefix(n, "Guest Profile") {
109+
addStore(filepath.Join(base, n), "Browser Profile: "+n)
119110
}
120111
}
121112
}
122113

114+
// 4. System-wide NSS locations commonly used on Linux
115+
if runtime.GOOS != "windows" {
116+
for _, sysPath := range []string{"/etc/pki/nssdb", "/etc/nssdb"} {
117+
if _, err := os.Stat(filepath.Join(sysPath, "cert9.db")); err == nil {
118+
addStore(sysPath, "System NSS")
119+
}
120+
}
121+
}
122+
123+
// 5. Aggressive walk: look for cert9.db/cert8.db under all likely roots.
124+
walkRoots := uniqueExistingDirs(
125+
filepath.Join(home, ".pki"),
126+
filepath.Join(home, ".mozilla"),
127+
filepath.Join(home, ".thunderbird"),
128+
filepath.Join(home, ".librewolf"),
129+
filepath.Join(home, ".waterfox"),
130+
filepath.Join(home, ".var", "app"), // flatpak user data
131+
filepath.Join(home, "snap"), // snap user data
132+
filepath.Join(home, ".local", "share"),
133+
localAppDataDir(),
134+
appDataDir(),
135+
)
136+
if runtime.GOOS == "darwin" {
137+
walkRoots = append(walkRoots, filepath.Join(home, "Library", "Application Support"))
138+
}
139+
if runtime.GOOS == "linux" {
140+
walkRoots = append(walkRoots,
141+
"/etc/pki",
142+
"/etc/ssl",
143+
)
144+
}
145+
candidates := walkNSSCandidates(ctx, walkRoots, 7, 500)
146+
for _, dir := range candidates {
147+
addStore(dir, "Browser NSS")
148+
}
149+
123150
return stores
124151
}
125152

@@ -168,10 +195,33 @@ func findNSSLib() string {
168195
}
169196
default:
170197
paths := []string{
198+
// Debian/Ubuntu multiarch
171199
"/usr/lib/x86_64-linux-gnu/libsoftokn3.so",
200+
"/usr/lib/x86_64-linux-gnu/nss/libsoftokn3.so",
201+
"/usr/lib/i386-linux-gnu/libsoftokn3.so",
202+
"/usr/lib/aarch64-linux-gnu/libsoftokn3.so",
203+
"/usr/lib/arm-linux-gnueabihf/libsoftokn3.so",
204+
// Generic / Fedora / RHEL / Arch
172205
"/usr/lib/libsoftokn3.so",
173206
"/usr/lib64/libsoftokn3.so",
174-
"/usr/lib/x86_64-linux-gnu/nss/libsoftokn3.so",
207+
"/usr/lib64/nss/libsoftokn3.so",
208+
// libnss3 fallback (some distros only ship this)
209+
"/usr/lib/x86_64-linux-gnu/libnss3.so",
210+
"/usr/lib/libnss3.so",
211+
"/usr/lib64/libnss3.so",
212+
// Firefox snap bundle
213+
"/snap/firefox/current/usr/lib/firefox/libsoftokn3.so",
214+
"/snap/firefox/current/usr/lib/firefox/libnss3.so",
215+
// Firefox flatpak bundle
216+
"/var/lib/flatpak/app/org.mozilla.firefox/current/active/files/lib/firefox/libsoftokn3.so",
217+
"/var/lib/flatpak/app/org.mozilla.firefox/current/active/files/lib/firefox/libnss3.so",
218+
}
219+
// Also check user-local flatpak
220+
if home, err := os.UserHomeDir(); err == nil {
221+
paths = append(paths,
222+
filepath.Join(home, ".local", "share", "flatpak", "app", "org.mozilla.firefox", "current", "active", "files", "lib", "firefox", "libsoftokn3.so"),
223+
filepath.Join(home, ".local", "share", "flatpak", "app", "org.mozilla.firefox", "current", "active", "files", "lib", "firefox", "libnss3.so"),
224+
)
175225
}
176226
for _, p := range paths {
177227
if _, err := os.Stat(p); err == nil {
@@ -182,6 +232,79 @@ func findNSSLib() string {
182232
return ""
183233
}
184234

235+
func uniqueExistingDirs(dirs ...string) []string {
236+
seen := make(map[string]struct{})
237+
var out []string
238+
for _, d := range dirs {
239+
if d == "" {
240+
continue
241+
}
242+
d = filepath.Clean(d)
243+
if _, err := os.Stat(d); err != nil {
244+
continue
245+
}
246+
if _, ok := seen[d]; ok {
247+
continue
248+
}
249+
seen[d] = struct{}{}
250+
out = append(out, d)
251+
}
252+
return out
253+
}
254+
255+
func walkNSSCandidates(ctx context.Context, roots []string, maxDepth int, limit int) []string {
256+
type void struct{}
257+
seen := make(map[string]void)
258+
var results []string
259+
for _, root := range roots {
260+
select {
261+
case <-ctx.Done():
262+
return results
263+
default:
264+
}
265+
root = filepath.Clean(root)
266+
rootDepth := len(strings.Split(root, string(os.PathSeparator)))
267+
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
268+
if err != nil {
269+
return nil
270+
}
271+
select {
272+
case <-ctx.Done():
273+
return context.Canceled
274+
default:
275+
}
276+
depth := len(strings.Split(path, string(os.PathSeparator))) - rootDepth
277+
if depth > maxDepth {
278+
if d.IsDir() {
279+
return filepath.SkipDir
280+
}
281+
return nil
282+
}
283+
if d.IsDir() {
284+
return nil
285+
}
286+
name := d.Name()
287+
if name != "cert9.db" && name != "cert8.db" {
288+
return nil
289+
}
290+
dir := filepath.Dir(path)
291+
if _, ok := seen[dir]; ok {
292+
return nil
293+
}
294+
seen[dir] = void{}
295+
results = append(results, dir)
296+
if limit > 0 && len(results) >= limit {
297+
return context.Canceled
298+
}
299+
return nil
300+
})
301+
if limit > 0 && len(results) >= limit {
302+
break
303+
}
304+
}
305+
return results
306+
}
307+
185308
func (s *NSSStore) List(ctx context.Context) ([]pkcs12store.Identity, error) {
186309
return s.listViaWorker(ctx)
187310
}

0 commit comments

Comments
 (0)