-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathextract_password.go
More file actions
103 lines (88 loc) · 2.88 KB
/
extract_password.go
File metadata and controls
103 lines (88 loc) · 2.88 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
package safari
import (
"fmt"
"sort"
"strings"
"github.com/moond4rk/keychainbreaker"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
)
func extractPasswords(keychainPassword string) ([]types.LoginEntry, error) {
passwords, err := getInternetPasswords(keychainPassword)
if err != nil {
return nil, err
}
var logins []types.LoginEntry
for _, p := range passwords {
url := buildURL(p.Protocol, p.Server, p.Port, p.Path)
if url == "" || p.Account == "" {
continue
}
logins = append(logins, types.LoginEntry{
URL: url,
Username: p.Account,
Password: p.PlainPassword,
CreatedAt: p.Created.UTC(),
})
}
sort.Slice(logins, func(i, j int) bool {
return logins[i].CreatedAt.After(logins[j].CreatedAt)
})
return logins, nil
}
func countPasswords(keychainPassword string) (int, error) {
passwords, err := extractPasswords(keychainPassword)
if err != nil {
return 0, err
}
return len(passwords), nil
}
// getInternetPasswords reads InternetPassword records directly from the
// macOS login keychain. See rfcs/006-key-retrieval-mechanisms.md §7 for why
// Safari owns this path instead of routing through crypto/keyretriever.
//
// TryUnlock is always invoked — with the user-supplied password when one is
// available, otherwise with no options — to enable keychainbreaker's partial
// extraction mode. With a valid password we get fully decrypted entries; with
// empty or wrong password we still get metadata records (URL, account,
// timestamps) and PlainPassword left blank, which Safari can export as
// metadata-only output instead of failing with ErrLocked.
func getInternetPasswords(keychainPassword string) ([]keychainbreaker.InternetPassword, error) {
kc, err := keychainbreaker.Open()
if err != nil {
return nil, fmt.Errorf("open keychain: %w", err)
}
var unlockOpts []keychainbreaker.UnlockOption
if keychainPassword != "" {
unlockOpts = append(unlockOpts, keychainbreaker.WithPassword(keychainPassword))
}
if err := kc.TryUnlock(unlockOpts...); err != nil {
log.Debugf("keychain unlock detail: %v", err)
}
passwords, err := kc.InternetPasswords()
if err != nil {
return nil, fmt.Errorf("extract internet passwords: %w", err)
}
return passwords, nil
}
// buildURL constructs a URL from InternetPassword fields.
func buildURL(protocol, server string, port uint32, path string) string {
if server == "" {
return ""
}
// Convert macOS Keychain FourCC protocol code to URL scheme.
// Only "htps" needs special mapping; others just need space trimming.
scheme := strings.TrimRight(protocol, " ")
if scheme == "" || scheme == "htps" {
scheme = "https"
}
url := scheme + "://" + server
defaultPorts := map[string]uint32{"https": 443, "http": 80, "ftp": 21}
if port > 0 && port != defaultPorts[scheme] {
url += fmt.Sprintf(":%d", port)
}
if path != "" && path != "/" {
url += path
}
return url
}