diff --git a/.typos.toml b/.typos.toml index 1795e74..4d7a695 100644 --- a/.typos.toml +++ b/.typos.toml @@ -5,5 +5,6 @@ Sie = "Sie" OT = "OT" Encrypter = "Encrypter" Decrypter = "Decrypter" +PASSWOR = "PASSWOR" [files] extend-exclude = ["go.mod", "go.sum"] \ No newline at end of file diff --git a/browser/browser.go b/browser/browser.go index bc7736c..b74ffa1 100644 --- a/browser/browser.go +++ b/browser/browser.go @@ -48,7 +48,7 @@ func pickFromConfigs(configs []types.BrowserConfig, opts PickOptions) ([]Browser } if opts.ProfilePath != "" && name != "all" { - if cfg.Kind == types.KindFirefox { + if cfg.Kind == types.Firefox { cfg.UserDataDir = filepath.Dir(filepath.Clean(opts.ProfilePath)) } else { cfg.UserDataDir = opts.ProfilePath @@ -76,7 +76,7 @@ func pickFromConfigs(configs []types.BrowserConfig, opts PickOptions) ([]Browser // newBrowsers dispatches to the correct engine based on BrowserKind. func newBrowsers(cfg types.BrowserConfig) ([]Browser, error) { switch cfg.Kind { - case types.KindChromium, types.KindChromiumYandex, types.KindChromiumOpera: + case types.Chromium, types.ChromiumYandex, types.ChromiumOpera: bs, err := chromium.NewBrowsers(cfg) if err != nil { return nil, err @@ -87,7 +87,7 @@ func newBrowsers(cfg types.BrowserConfig) ([]Browser, error) { } return browsers, nil - case types.KindFirefox: + case types.Firefox: bs, err := firefox.NewBrowsers(cfg) if err != nil { return nil, err diff --git a/browser/browser_darwin.go b/browser/browser_darwin.go index fc8ca17..f884fc4 100644 --- a/browser/browser_darwin.go +++ b/browser/browser_darwin.go @@ -11,84 +11,84 @@ func platformBrowsers() []types.BrowserConfig { { Key: "chrome", Name: chromeName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Chrome", UserDataDir: homeDir + "/Library/Application Support/Google/Chrome", }, { Key: "edge", Name: edgeName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Microsoft Edge", UserDataDir: homeDir + "/Library/Application Support/Microsoft Edge", }, { Key: "chromium", Name: chromiumName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Chromium", UserDataDir: homeDir + "/Library/Application Support/Chromium", }, { Key: "chrome-beta", Name: chromeBetaName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Chrome", UserDataDir: homeDir + "/Library/Application Support/Google/Chrome Beta", }, { Key: "opera", Name: operaName, - Kind: types.KindChromiumOpera, + Kind: types.ChromiumOpera, Storage: "Opera", UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.Opera", }, { Key: "opera-gx", Name: operaGXName, - Kind: types.KindChromiumOpera, + Kind: types.ChromiumOpera, Storage: "Opera", UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.OperaGX", }, { Key: "vivaldi", Name: vivaldiName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Vivaldi", UserDataDir: homeDir + "/Library/Application Support/Vivaldi", }, { Key: "coccoc", Name: coccocName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "CocCoc", UserDataDir: homeDir + "/Library/Application Support/Coccoc", }, { Key: "brave", Name: braveName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Brave", UserDataDir: homeDir + "/Library/Application Support/BraveSoftware/Brave-Browser", }, { Key: "yandex", Name: yandexName, - Kind: types.KindChromiumYandex, + Kind: types.ChromiumYandex, Storage: "Yandex", UserDataDir: homeDir + "/Library/Application Support/Yandex/YandexBrowser", }, { Key: "arc", Name: arcName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Arc", UserDataDir: homeDir + "/Library/Application Support/Arc/User Data", }, { Key: "firefox", Name: firefoxName, - Kind: types.KindFirefox, + Kind: types.Firefox, UserDataDir: homeDir + "/Library/Application Support/Firefox/Profiles", }, } diff --git a/browser/browser_linux.go b/browser/browser_linux.go index ffb25df..d9d28bc 100644 --- a/browser/browser_linux.go +++ b/browser/browser_linux.go @@ -11,56 +11,56 @@ func platformBrowsers() []types.BrowserConfig { { Key: "chrome", Name: chromeName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Chrome Safe Storage", UserDataDir: homeDir + "/.config/google-chrome", }, { Key: "edge", Name: edgeName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Chromium Safe Storage", UserDataDir: homeDir + "/.config/microsoft-edge", }, { Key: "chromium", Name: chromiumName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Chromium Safe Storage", UserDataDir: homeDir + "/.config/chromium", }, { Key: "chrome-beta", Name: chromeBetaName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Chrome Safe Storage", UserDataDir: homeDir + "/.config/google-chrome-beta", }, { Key: "opera", Name: operaName, - Kind: types.KindChromiumOpera, + Kind: types.ChromiumOpera, Storage: "Chromium Safe Storage", UserDataDir: homeDir + "/.config/opera", }, { Key: "vivaldi", Name: vivaldiName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Chrome Safe Storage", UserDataDir: homeDir + "/.config/vivaldi", }, { Key: "brave", Name: braveName, - Kind: types.KindChromium, + Kind: types.Chromium, Storage: "Brave Safe Storage", UserDataDir: homeDir + "/.config/BraveSoftware/Brave-Browser", }, { Key: "firefox", Name: firefoxName, - Kind: types.KindFirefox, + Kind: types.Firefox, UserDataDir: homeDir + "/.mozilla/firefox", }, } diff --git a/browser/browser_test.go b/browser/browser_test.go index 2899bdd..1931ebd 100644 --- a/browser/browser_test.go +++ b/browser/browser_test.go @@ -32,8 +32,8 @@ func TestPickFromConfigs_NameFilter(t *testing.T) { mkFile(t, dir, "Default", "History") configs := []types.BrowserConfig{ - {Key: "chrome", Name: "Chrome", Kind: types.KindChromium, UserDataDir: dir}, - {Key: "edge", Name: "Edge", Kind: types.KindChromium, UserDataDir: dir}, + {Key: "chrome", Name: "Chrome", Kind: types.Chromium, UserDataDir: dir}, + {Key: "edge", Name: "Edge", Kind: types.Chromium, UserDataDir: dir}, } tests := []struct { @@ -102,7 +102,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) { { name: "chromium multi-profile", configs: []types.BrowserConfig{ - {Key: "chrome", Name: "Chrome", Kind: types.KindChromium, UserDataDir: chromeDir}, + {Key: "chrome", Name: "Chrome", Kind: types.Chromium, UserDataDir: chromeDir}, }, wantNames: []string{"Chrome", "Chrome"}, wantProfiles: []string{"Default", "Profile 1"}, @@ -110,7 +110,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) { { name: "firefox random dir", configs: []types.BrowserConfig{ - {Key: "firefox", Name: "Firefox", Kind: types.KindFirefox, UserDataDir: firefoxDir}, + {Key: "firefox", Name: "Firefox", Kind: types.Firefox, UserDataDir: firefoxDir}, }, wantNames: []string{"Firefox"}, wantProfiles: []string{"abc123.default-release"}, @@ -118,7 +118,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) { { name: "yandex variant", configs: []types.BrowserConfig{ - {Key: "yandex", Name: "Yandex", Kind: types.KindChromiumYandex, UserDataDir: yandexDir}, + {Key: "yandex", Name: "Yandex", Kind: types.ChromiumYandex, UserDataDir: yandexDir}, }, wantNames: []string{"Yandex"}, wantProfiles: []string{"Default"}, @@ -126,7 +126,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) { { name: "nonexistent dir", configs: []types.BrowserConfig{ - {Key: "chrome", Name: "Chrome", Kind: types.KindChromium, UserDataDir: "/nonexistent"}, + {Key: "chrome", Name: "Chrome", Kind: types.Chromium, UserDataDir: "/nonexistent"}, }, }, } @@ -164,7 +164,7 @@ func TestPickFromConfigs_ProfilePath(t *testing.T) { { name: "chromium uses path directly", configs: []types.BrowserConfig{ - {Key: "chrome", Name: "Chrome", Kind: types.KindChromium, UserDataDir: "/wrong"}, + {Key: "chrome", Name: "Chrome", Kind: types.Chromium, UserDataDir: "/wrong"}, }, pickName: "chrome", profilePath: filepath.Join(chromeDir, "Default"), @@ -174,7 +174,7 @@ func TestPickFromConfigs_ProfilePath(t *testing.T) { { name: "firefox uses parent dir", configs: []types.BrowserConfig{ - {Key: "firefox", Name: "Firefox", Kind: types.KindFirefox, UserDataDir: "/wrong"}, + {Key: "firefox", Name: "Firefox", Kind: types.Firefox, UserDataDir: "/wrong"}, }, pickName: "firefox", profilePath: filepath.Join(firefoxDir, "abc123.default-release"), @@ -184,7 +184,7 @@ func TestPickFromConfigs_ProfilePath(t *testing.T) { { name: "ignored when name is all", configs: []types.BrowserConfig{ - {Key: "chrome", Name: "Chrome", Kind: types.KindChromium, UserDataDir: chromeDir}, + {Key: "chrome", Name: "Chrome", Kind: types.Chromium, UserDataDir: chromeDir}, }, pickName: "all", profilePath: "/some/override", diff --git a/browser/browser_windows.go b/browser/browser_windows.go index ce5d986..c335e94 100644 --- a/browser/browser_windows.go +++ b/browser/browser_windows.go @@ -11,97 +11,97 @@ func platformBrowsers() []types.BrowserConfig { { Key: "chrome", Name: chromeName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/Google/Chrome/User Data", }, { Key: "edge", Name: edgeName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/Microsoft/Edge/User Data", }, { Key: "chromium", Name: chromiumName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/Chromium/User Data", }, { Key: "chrome-beta", Name: chromeBetaName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/Google/Chrome Beta/User Data", }, { Key: "opera", Name: operaName, - Kind: types.KindChromiumOpera, + Kind: types.ChromiumOpera, UserDataDir: homeDir + "/AppData/Roaming/Opera Software/Opera Stable", }, { Key: "opera-gx", Name: operaGXName, - Kind: types.KindChromiumOpera, + Kind: types.ChromiumOpera, UserDataDir: homeDir + "/AppData/Roaming/Opera Software/Opera GX Stable", }, { Key: "vivaldi", Name: vivaldiName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/Vivaldi/User Data", }, { Key: "coccoc", Name: coccocName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/CocCoc/Browser/User Data", }, { Key: "brave", Name: braveName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/BraveSoftware/Brave-Browser/User Data", }, { Key: "yandex", Name: yandexName, - Kind: types.KindChromiumYandex, + Kind: types.ChromiumYandex, UserDataDir: homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data", }, { Key: "360x", Name: speed360XName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/360ChromeX/Chrome/User Data", }, { Key: "360", Name: speed360Name, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/360chrome/Chrome/User Data", }, { Key: "qq", - Name: qqBrowserName, - Kind: types.KindChromium, + Name: qqName, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/Tencent/QQBrowser/User Data", }, { Key: "dc", - Name: dcBrowserName, - Kind: types.KindChromium, + Name: dcName, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/DCBrowser/User Data", }, { Key: "sogou", Name: sogouName, - Kind: types.KindChromium, + Kind: types.Chromium, UserDataDir: homeDir + "/AppData/Local/Sogou/SogouExplorer/User Data", }, { Key: "firefox", Name: firefoxName, - Kind: types.KindFirefox, + Kind: types.Firefox, UserDataDir: homeDir + "/AppData/Roaming/Mozilla/Firefox/Profiles", }, } diff --git a/browser/chromium/chromium.go b/browser/chromium/chromium.go index 0fab0f3..cc030d8 100644 --- a/browser/chromium/chromium.go +++ b/browser/chromium/chromium.go @@ -3,6 +3,7 @@ package chromium import ( "os" "path/filepath" + "time" "github.com/moond4rk/hackbrowserdata/crypto/keyretriever" "github.com/moond4rk/hackbrowserdata/filemanager" @@ -125,7 +126,7 @@ func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) { var localStateDst string for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} { candidate := filepath.Join(dir, "Local State") - if fileutil.IsFileExists(candidate) { + if fileutil.FileExists(candidate) { localStateDst = filepath.Join(session.TempDir(), "Local State") if err := session.Acquire(candidate, localStateDst, false); err != nil { return nil, err @@ -273,3 +274,18 @@ func isSkippedDir(name string) bool { } return false } + +// timeEpoch converts a WebKit/Chromium epoch timestamp (microseconds since +// 1601-01-01) to a time.Time. +func timeEpoch(epoch int64) time.Time { + maxTime := int64(99633311740000000) + if epoch > maxTime { + return time.Date(2049, 1, 1, 1, 1, 1, 1, time.Local) + } + t := time.Date(1601, 1, 1, 0, 0, 0, 0, time.Local) + d := time.Duration(epoch) + for i := 0; i < 1000; i++ { + t = t.Add(d) + } + return t +} diff --git a/browser/chromium/chromium_test.go b/browser/chromium/chromium_test.go index 182df8d..3d1defc 100644 --- a/browser/chromium/chromium_test.go +++ b/browser/chromium/chromium_test.go @@ -142,7 +142,7 @@ func TestNewBrowsers(t *testing.T) { { name: "chrome multi-profile", dir: fixture.chrome, - kind: types.KindChromium, + kind: types.Chromium, wantProfiles: []string{"Default", "Profile 1", "Profile 3"}, wantCats: map[string][]string{ "Default": {"Login Data", "Cookies", "History", "Bookmarks", "Web Data", "Secure Preferences", "leveldb", "Session Storage"}, @@ -153,7 +153,7 @@ func TestNewBrowsers(t *testing.T) { { name: "opera with Default", dir: fixture.opera, - kind: types.KindChromium, + kind: types.Chromium, wantProfiles: []string{"Default"}, wantCats: map[string][]string{ "Default": {"Login Data", "History", "Bookmarks", "Cookies"}, @@ -162,7 +162,7 @@ func TestNewBrowsers(t *testing.T) { { name: "opera flat layout", dir: fixture.operaFlat, - kind: types.KindChromium, + kind: types.Chromium, wantProfiles: []string{filepath.Base(fixture.operaFlat)}, // userDataDir itself wantCats: map[string][]string{ filepath.Base(fixture.operaFlat): {"Login Data", "History", "Cookies"}, @@ -171,7 +171,7 @@ func TestNewBrowsers(t *testing.T) { { name: "yandex custom files", dir: fixture.yandex, - kind: types.KindChromiumYandex, + kind: types.ChromiumYandex, wantProfiles: []string{"Default"}, wantCats: map[string][]string{ "Default": {"Ya Passman Data", "Ya Credit Cards", "History", "Cookies", "Bookmarks"}, @@ -180,38 +180,38 @@ func TestNewBrowsers(t *testing.T) { { name: "old cookies fallback", dir: fixture.oldCookies, - kind: types.KindChromium, + kind: types.Chromium, wantProfiles: []string{"Default"}, }, { name: "cookie priority", dir: fixture.bothCookies, - kind: types.KindChromium, + kind: types.Chromium, wantProfiles: []string{"Default"}, }, { name: "leveldb directories", dir: fixture.leveldb, - kind: types.KindChromium, + kind: types.Chromium, wantProfiles: []string{"Default"}, wantDirs: []types.Category{types.LocalStorage, types.SessionStorage}, }, { name: "leveldb only", dir: fixture.leveldbOnly, - kind: types.KindChromium, + kind: types.Chromium, wantProfiles: []string{"Default"}, wantDirs: []types.Category{types.LocalStorage, types.SessionStorage}, }, { name: "empty dir", dir: fixture.empty, - kind: types.KindChromium, + kind: types.Chromium, }, { name: "nonexistent dir", dir: "/nonexistent/path", - kind: types.KindChromium, + kind: types.Chromium, }, } @@ -321,8 +321,8 @@ func TestSharedSourceFile(t *testing.T) { // --------------------------------------------------------------------------- func TestSourcesForKind(t *testing.T) { - chromium := sourcesForKind(types.KindChromium) - yandex := sourcesForKind(types.KindChromiumYandex) + chromium := sourcesForKind(types.Chromium) + yandex := sourcesForKind(types.ChromiumYandex) assert.Equal(t, "Login Data", chromium[types.Password][0].rel) assert.Equal(t, "Ya Passman Data", yandex[types.Password][0].rel) @@ -331,13 +331,13 @@ func TestSourcesForKind(t *testing.T) { } func TestExtractorsForKind(t *testing.T) { - assert.Nil(t, extractorsForKind(types.KindChromium)) + assert.Nil(t, extractorsForKind(types.Chromium)) - yandexExt := extractorsForKind(types.KindChromiumYandex) + yandexExt := extractorsForKind(types.ChromiumYandex) require.NotNil(t, yandexExt) assert.Contains(t, yandexExt, types.Password) - operaExt := extractorsForKind(types.KindChromiumOpera) + operaExt := extractorsForKind(types.ChromiumOpera) require.NotNil(t, operaExt) assert.Contains(t, operaExt, types.Extension) } @@ -425,7 +425,7 @@ func TestLocalStatePath(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - browsers, err := NewBrowsers(types.BrowserConfig{Name: "Test", Kind: types.KindChromium, UserDataDir: tt.dir}) + browsers, err := NewBrowsers(types.BrowserConfig{Name: "Test", Kind: types.Chromium, UserDataDir: tt.dir}) require.NoError(t, err) require.NotEmpty(t, browsers) diff --git a/browser/chromium/decrypt.go b/browser/chromium/decrypt.go index 3b0c5da..50cedeb 100644 --- a/browser/chromium/decrypt.go +++ b/browser/chromium/decrypt.go @@ -17,12 +17,12 @@ func decryptValue(masterKey, ciphertext []byte) ([]byte, error) { version := crypto.DetectVersion(ciphertext) switch version { case crypto.CipherV10: - return crypto.DecryptWithChromium(masterKey, ciphertext) + return crypto.DecryptChromium(masterKey, ciphertext) case crypto.CipherV20: // TODO: implement App-Bound Encryption (Chrome 127+) return nil, fmt.Errorf("v20 App-Bound Encryption not yet supported") case crypto.CipherDPAPI: - return crypto.DecryptWithDPAPI(ciphertext) + return crypto.DecryptDPAPI(ciphertext) default: return nil, fmt.Errorf("unsupported cipher version: %s", version) } diff --git a/browser/chromium/decrypt_test.go b/browser/chromium/decrypt_test.go index b0cc89d..f15e6a6 100644 --- a/browser/chromium/decrypt_test.go +++ b/browser/chromium/decrypt_test.go @@ -12,14 +12,12 @@ import ( "github.com/moond4rk/hackbrowserdata/crypto" ) -// testCBCIV is the fixed IV Chrome uses on macOS/Linux (16 space bytes). -var testCBCIV = bytes.Repeat([]byte{0x20}, 16) - func TestDecryptValue_V10(t *testing.T) { plaintext := []byte("test_secret_value") - encrypted, err := crypto.AES128CBCEncrypt(testAESKey, testCBCIV, plaintext) + testCBCIV := bytes.Repeat([]byte{0x20}, 16) + cbcEncrypted, err := crypto.AESCBCEncrypt(testAESKey, testCBCIV, plaintext) require.NoError(t, err) - v10Ciphertext := append([]byte("v10"), encrypted...) + v10Ciphertext := append([]byte("v10"), cbcEncrypted...) tests := []struct { name string @@ -35,7 +33,7 @@ func TestDecryptValue_V10(t *testing.T) { { name: "wrong key returns padding error", key: []byte("wrong_key_1234!!"), - wantErrMsg: "pkcs5UnPadding", + wantErrMsg: "invalid PKCS5 padding", }, } diff --git a/browser/chromium/decrypt_windows_test.go b/browser/chromium/decrypt_windows_test.go index 6e92dc5..c7e58b4 100644 --- a/browser/chromium/decrypt_windows_test.go +++ b/browser/chromium/decrypt_windows_test.go @@ -15,7 +15,7 @@ import ( ) // encryptWithDPAPI encrypts data using Windows DPAPI (CryptProtectData). -// This is the reverse of crypto.DecryptWithDPAPI, used only for testing. +// This is the reverse of DecryptDPAPI, used only for testing. func encryptWithDPAPI(plaintext []byte) ([]byte, error) { crypt32 := syscall.NewLazyDLL("Crypt32.dll") kernel32 := syscall.NewLazyDLL("Kernel32.dll") @@ -57,11 +57,11 @@ func TestDecryptValue_V10_Windows(t *testing.T) { plaintext := []byte("test_secret_value") nonce := []byte("123456789012") // 12-byte nonce - encrypted, err := crypto.AESGCMEncrypt(testAESKey, nonce, plaintext) + gcmEncrypted, err := crypto.AESGCMEncrypt(testAESKey, nonce, plaintext) require.NoError(t, err) // v10 format on Windows: "v10" + nonce(12) + encrypted - ciphertext := append([]byte("v10"), append(nonce, encrypted...)...) + ciphertext := append([]byte("v10"), append(nonce, gcmEncrypted...)...) got, err := decryptValue(testAESKey, ciphertext) require.NoError(t, err) diff --git a/browser/chromium/extract_bookmark.go b/browser/chromium/extract_bookmark.go index d61c2cf..2894612 100644 --- a/browser/chromium/extract_bookmark.go +++ b/browser/chromium/extract_bookmark.go @@ -7,7 +7,6 @@ import ( "github.com/tidwall/gjson" "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) func extractBookmarks(path string) ([]types.BookmarkEntry, error) { @@ -39,7 +38,7 @@ func walkBookmarks(node gjson.Result, folder string, out *[]types.BookmarkEntry) Type: nodeType, URL: node.Get("url").String(), Folder: folder, - CreatedAt: typeutil.TimeEpoch(node.Get("date_added").Int()), + CreatedAt: timeEpoch(node.Get("date_added").Int()), }) } diff --git a/browser/chromium/extract_cookie.go b/browser/chromium/extract_cookie.go index b6c696b..a7a34f1 100644 --- a/browser/chromium/extract_cookie.go +++ b/browser/chromium/extract_cookie.go @@ -9,7 +9,6 @@ import ( "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const defaultCookieQuery = `SELECT name, encrypted_value, host_key, path, @@ -46,8 +45,8 @@ func extractCookies(masterKey []byte, path string) ([]types.CookieEntry, error) IsHTTPOnly: isHTTPOnly != 0, HasExpire: hasExpire != 0, IsPersistent: isPersistent != 0, - ExpireAt: typeutil.TimeEpoch(expireAt), - CreatedAt: typeutil.TimeEpoch(createdAt), + ExpireAt: timeEpoch(expireAt), + CreatedAt: timeEpoch(createdAt), }, nil }) if err != nil { diff --git a/browser/chromium/extract_creditcard.go b/browser/chromium/extract_creditcard.go index 3cf7fe9..fe4c80b 100644 --- a/browser/chromium/extract_creditcard.go +++ b/browser/chromium/extract_creditcard.go @@ -14,9 +14,9 @@ const defaultCreditCardQuery = `SELECT COALESCE(guid, ''), name_on_card, expirat func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry, error) { return sqliteutil.QueryRows(path, false, defaultCreditCardQuery, func(rows *sql.Rows) (types.CreditCardEntry, error) { - var guid, name, month, year, nickName, address string + var guid, name, month, year, nickname, address string var encNumber []byte - if err := rows.Scan(&guid, &name, &month, &year, &encNumber, &nickName, &address); err != nil { + if err := rows.Scan(&guid, &name, &month, &year, &encNumber, &nickname, &address); err != nil { return types.CreditCardEntry{}, err } number, err := decryptValue(masterKey, encNumber) @@ -29,7 +29,7 @@ func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry, Number: string(number), ExpMonth: month, ExpYear: year, - NickName: nickName, + NickName: nickname, Address: address, }, nil }) diff --git a/browser/chromium/extract_download.go b/browser/chromium/extract_download.go index c4725fe..484a195 100644 --- a/browser/chromium/extract_download.go +++ b/browser/chromium/extract_download.go @@ -6,7 +6,6 @@ import ( "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const defaultDownloadQuery = `SELECT target_path, tab_url, total_bytes, start_time, end_time, @@ -25,8 +24,8 @@ func extractDownloads(path string) ([]types.DownloadEntry, error) { TargetPath: targetPath, MimeType: mimeType, TotalBytes: totalBytes, - StartTime: typeutil.TimeEpoch(startTime), - EndTime: typeutil.TimeEpoch(endTime), + StartTime: timeEpoch(startTime), + EndTime: timeEpoch(endTime), }, nil }) if err != nil { diff --git a/browser/chromium/extract_history.go b/browser/chromium/extract_history.go index 58f711e..39ce189 100644 --- a/browser/chromium/extract_history.go +++ b/browser/chromium/extract_history.go @@ -6,7 +6,6 @@ import ( "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const defaultHistoryQuery = `SELECT url, title, visit_count, last_visit_time FROM urls` @@ -24,7 +23,7 @@ func extractHistories(path string) ([]types.HistoryEntry, error) { URL: url, Title: title, VisitCount: visitCount, - LastVisit: typeutil.TimeEpoch(lastVisit), + LastVisit: timeEpoch(lastVisit), }, nil }) if err != nil { diff --git a/browser/chromium/extract_password.go b/browser/chromium/extract_password.go index 37e46a8..45e762a 100644 --- a/browser/chromium/extract_password.go +++ b/browser/chromium/extract_password.go @@ -7,7 +7,6 @@ import ( "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const defaultLoginQuery = `SELECT origin_url, username_value, password_value, date_created FROM logins` @@ -33,7 +32,7 @@ func extractPasswordsWithQuery(masterKey []byte, path, query string) ([]types.Lo URL: url, Username: username, Password: string(password), - CreatedAt: typeutil.TimeEpoch(created), + CreatedAt: timeEpoch(created), }, nil }) if err != nil { diff --git a/browser/chromium/source.go b/browser/chromium/source.go index e6301a9..ef5af14 100644 --- a/browser/chromium/source.go +++ b/browser/chromium/source.go @@ -52,7 +52,7 @@ func yandexSources() map[types.Category][]sourcePath { // sourcesForKind returns the source mapping for a browser kind. func sourcesForKind(kind types.BrowserKind) map[types.Category][]sourcePath { switch kind { - case types.KindChromiumYandex: + case types.ChromiumYandex: return yandexSources() default: return chromiumSources @@ -109,9 +109,9 @@ var operaExtractors = map[types.Category]categoryExtractor{ // nil means all categories use the default extractCategory switch logic. func extractorsForKind(kind types.BrowserKind) map[types.Category]categoryExtractor { switch kind { - case types.KindChromiumYandex: + case types.ChromiumYandex: return yandexExtractors - case types.KindChromiumOpera: + case types.ChromiumOpera: return operaExtractors default: return nil diff --git a/browser/consts.go b/browser/consts.go index be24206..7e53e4e 100644 --- a/browser/consts.go +++ b/browser/consts.go @@ -21,8 +21,8 @@ const ( firefoxName = "Firefox" speed360Name = "360 Speed" speed360XName = "360 Speed X" - qqBrowserName = "QQ" - dcBrowserName = "DC" + qqName = "QQ" + dcName = "DC" sogouName = "Sogou" arcName = "Arc" ) diff --git a/browser/firefox/extract_bookmark.go b/browser/firefox/extract_bookmark.go index 437c03f..731d2d6 100644 --- a/browser/firefox/extract_bookmark.go +++ b/browser/firefox/extract_bookmark.go @@ -6,7 +6,6 @@ import ( "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const firefoxBookmarkQuery = `SELECT id, url, type, dateAdded, COALESCE(title, '') @@ -25,7 +24,7 @@ func extractBookmarks(path string) ([]types.BookmarkEntry, error) { Name: title, URL: url, Folder: bookmarkType(bt), - CreatedAt: typeutil.TimeStamp(dateAdded / 1000000), + CreatedAt: timestamp(dateAdded / 1000000), }, nil }) if err != nil { diff --git a/browser/firefox/extract_cookie.go b/browser/firefox/extract_cookie.go index d653ea9..c5d03ed 100644 --- a/browser/firefox/extract_cookie.go +++ b/browser/firefox/extract_cookie.go @@ -6,7 +6,6 @@ import ( "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const firefoxCookieQuery = `SELECT name, value, host, path, @@ -34,8 +33,8 @@ func extractCookies(path string) ([]types.CookieEntry, error) { IsHTTPOnly: isHTTPOnly != 0, HasExpire: hasExpire, IsPersistent: hasExpire, - ExpireAt: typeutil.TimeStamp(expiry), - CreatedAt: typeutil.TimeStamp(createdAt / 1000000), + ExpireAt: timestamp(expiry), + CreatedAt: timestamp(createdAt / 1000000), }, nil }) if err != nil { diff --git a/browser/firefox/extract_download.go b/browser/firefox/extract_download.go index e7abaae..75a342d 100644 --- a/browser/firefox/extract_download.go +++ b/browser/firefox/extract_download.go @@ -9,7 +9,6 @@ import ( "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const firefoxDownloadQuery = `SELECT place_id, GROUP_CONCAT(content), url, dateAdded @@ -27,7 +26,7 @@ func extractDownloads(path string) ([]types.DownloadEntry, error) { entry := types.DownloadEntry{ URL: url, - StartTime: typeutil.TimeStamp(dateAdded / 1000000), + StartTime: timestamp(dateAdded / 1000000), } // Firefox stores download metadata as: "target_path,{json}" @@ -37,7 +36,7 @@ func extractDownloads(path string) ([]types.DownloadEntry, error) { entry.TargetPath = contentList[0] json := "{" + contentList[1] entry.TotalBytes = gjson.Get(json, "fileSize").Int() - entry.EndTime = typeutil.TimeStamp(gjson.Get(json, "endTime").Int() / 1000) + entry.EndTime = timestamp(gjson.Get(json, "endTime").Int() / 1000) } else { entry.TargetPath = content } diff --git a/browser/firefox/extract_history.go b/browser/firefox/extract_history.go index 7683de4..c79c057 100644 --- a/browser/firefox/extract_history.go +++ b/browser/firefox/extract_history.go @@ -6,7 +6,6 @@ import ( "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const firefoxHistoryQuery = `SELECT url, COALESCE(last_visit_date, 0), @@ -25,7 +24,7 @@ func extractHistories(path string) ([]types.HistoryEntry, error) { URL: url, Title: title, VisitCount: visitCount, - LastVisit: typeutil.TimeStamp(lastVisit / 1000000), + LastVisit: timestamp(lastVisit / 1000000), }, nil }) if err != nil { diff --git a/browser/firefox/extract_password.go b/browser/firefox/extract_password.go index e0f3ddb..cb67f19 100644 --- a/browser/firefox/extract_password.go +++ b/browser/firefox/extract_password.go @@ -11,7 +11,6 @@ import ( "github.com/moond4rk/hackbrowserdata/crypto" "github.com/moond4rk/hackbrowserdata/log" "github.com/moond4rk/hackbrowserdata/types" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) // decryptPBE combines base64 decode + ASN1 PBE parse + decrypt into one call. @@ -57,7 +56,7 @@ func extractPasswords(masterKey []byte, path string) ([]types.LoginEntry, error) URL: url, Username: string(user), Password: string(pwd), - CreatedAt: typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000), + CreatedAt: timestamp(v.Get("timeCreated").Int() / 1000), }) } diff --git a/browser/firefox/extract_storage.go b/browser/firefox/extract_storage.go index 2ab6e11..90f9685 100644 --- a/browser/firefox/extract_storage.go +++ b/browser/firefox/extract_storage.go @@ -7,7 +7,6 @@ import ( "github.com/moond4rk/hackbrowserdata/types" "github.com/moond4rk/hackbrowserdata/utils/sqliteutil" - "github.com/moond4rk/hackbrowserdata/utils/typeutil" ) const firefoxLocalStorageQuery = `SELECT originKey, key, value FROM webappsstore2` @@ -27,6 +26,14 @@ func extractLocalStorage(path string) ([]types.StorageEntry, error) { }) } +func reverseString(s string) string { + b := []byte(s) + for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + return string(b) +} + // parseOriginKey converts Firefox's reversed origin format to a URL. // Example: "moc.buhtig.:https:443" → "https://github.com:443" func parseOriginKey(originKey string) string { @@ -34,7 +41,7 @@ func parseOriginKey(originKey string) string { if len(parts) < 2 { return originKey } - host := string(typeutil.Reverse([]byte(parts[0]))) + host := reverseString(parts[0]) host = strings.TrimPrefix(host, ".") scheme := parts[1] if len(parts) == 3 { diff --git a/browser/firefox/firefox.go b/browser/firefox/firefox.go index 886dd77..9b62bfa 100644 --- a/browser/firefox/firefox.go +++ b/browser/firefox/firefox.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "time" "github.com/moond4rk/hackbrowserdata/filemanager" "github.com/moond4rk/hackbrowserdata/log" @@ -106,7 +107,7 @@ func (b *Browser) acquireFiles(session *filemanager.Session, categories []types. // is validated by attempting to decrypt an actual login entry. func (b *Browser) getMasterKey(session *filemanager.Session, tempPaths map[types.Category]string) ([]byte, error) { key4Src := filepath.Join(b.profileDir, "key4.db") - if !fileutil.IsFileExists(key4Src) { + if !fileutil.FileExists(key4Src) { return nil, nil } key4Dst := filepath.Join(session.TempDir(), "key4.db") @@ -236,3 +237,12 @@ func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir stri } return resolved } + +// timestamp converts a Unix epoch timestamp (seconds) to a time.Time. +func timestamp(stamp int64) time.Time { + s := time.Unix(stamp, 0) + if s.Local().Year() > 9999 { + return time.Date(9999, 12, 13, 23, 59, 59, 0, time.Local) + } + return s +} diff --git a/browser/firefox/firefox_test.go b/browser/firefox/firefox_test.go index 50f8c04..6b0e6d5 100644 --- a/browser/firefox/firefox_test.go +++ b/browser/firefox/firefox_test.go @@ -115,7 +115,7 @@ func TestNewBrowsers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cfg := types.BrowserConfig{Name: "Firefox", Kind: types.KindFirefox, UserDataDir: tt.dir} + cfg := types.BrowserConfig{Name: "Firefox", Kind: types.Firefox, UserDataDir: tt.dir} browsers, err := NewBrowsers(cfg) require.NoError(t, err) diff --git a/crypto/asn1pbe.go b/crypto/asn1pbe.go index b4b8f3f..3fddc23 100644 --- a/crypto/asn1pbe.go +++ b/crypto/asn1pbe.go @@ -1,24 +1,30 @@ package crypto import ( + "crypto/aes" + "crypto/des" "crypto/hmac" "crypto/sha1" "crypto/sha256" "encoding/asn1" - "errors" ) -type ASN1PBE interface { - Decrypt(globalSalt []byte) ([]byte, error) +const des3KeySize = 24 // 3DES uses 24-byte (192-bit) keys - Encrypt(globalSalt, plaintext []byte) ([]byte, error) +// ASN1PBE represents a Password-Based Encryption structure from Firefox's NSS. +// The key parameter semantics vary by implementation: +// - privateKeyPBE / passwordCheckPBE: key is the global salt used for key derivation +// - credentialPBE: key is the already-derived master key +type ASN1PBE interface { + Decrypt(key []byte) ([]byte, error) + Encrypt(key, plaintext []byte) ([]byte, error) } func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) { var ( - nss nssPBE - meta metaPBE - login loginPBE + nss privateKeyPBE + meta passwordCheckPBE + login credentialPBE ) if _, err := asn1.Unmarshal(b, &nss); err == nil { return nss, nil @@ -29,12 +35,10 @@ func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) { if _, err := asn1.Unmarshal(b, &login); err == nil { return login, nil } - return nil, ErrDecodeASN1Failed + return nil, errDecodeASN1 } -var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed") - -// nssPBE Struct +// privateKeyPBE Struct // // SEQUENCE (2 elem) // OBJECT IDENTIFIER @@ -42,52 +46,56 @@ var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed") // OCTET STRING (20 byte) // INTEGER 1 // OCTET STRING (16 byte) -type nssPBE struct { +type privateKeyPBE struct { AlgoAttr struct { asn1.ObjectIdentifier SaltAttr struct { EntrySalt []byte - Len int + KeyLen int } } Encrypted []byte } -// Decrypt decrypts the encrypted password with the global salt. -func (n nssPBE) Decrypt(globalSalt []byte) ([]byte, error) { +func (n privateKeyPBE) Decrypt(globalSalt []byte) ([]byte, error) { key, iv := n.deriveKeyAndIV(globalSalt) - return DES3Decrypt(key, iv, n.Encrypted) } -func (n nssPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) { +func (n privateKeyPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) { key, iv := n.deriveKeyAndIV(globalSalt) - return DES3Encrypt(key, iv, plaintext) } -// deriveKeyAndIV derives the key and initialization vector (IV) -// from the global salt and entry salt. -func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { - salt := n.AlgoAttr.SaltAttr.EntrySalt - hashPrefix := sha1.Sum(globalSalt) - compositeHash := sha1.Sum(append(hashPrefix[:], salt...)) - paddedEntrySalt := paddingZero(salt, 20) +// deriveKeyAndIV implements NSS PBE-SHA1-3DES key derivation. +// Reference: https://searchfox.org/mozilla-central/source/security/nss/lib/softoken/lowpbe.c +// +// Derivation steps: +// +// hp = SHA1(globalSalt) +// ck = SHA1(hp || entrySalt) +// hmac1 = HMAC-SHA1(ck, paddedSalt) +// k1 = HMAC-SHA1(ck, paddedSalt || entrySalt) +// k2 = HMAC-SHA1(ck, hmac1 || entrySalt) +// dk = k1 || k2 (40 bytes) +// key = dk[:24], iv = dk[32:] +func (n privateKeyPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { + entrySalt := n.AlgoAttr.SaltAttr.EntrySalt + hp := sha1.Sum(globalSalt) + ck := sha1.Sum(append(hp[:], entrySalt...)) + paddedSalt := paddingZero(entrySalt, 20) - hmacProcessor := hmac.New(sha1.New, compositeHash[:]) - hmacProcessor.Write(paddedEntrySalt) + hmac1 := hmac.New(sha1.New, ck[:]) + hmac1.Write(paddedSalt) - paddedEntrySalt = append(paddedEntrySalt, salt...) - keyComponent1 := hmac.New(sha1.New, compositeHash[:]) - keyComponent1.Write(paddedEntrySalt) + k1 := hmac.New(sha1.New, ck[:]) + k1.Write(append(paddedSalt, entrySalt...)) - hmacWithSalt := append(hmacProcessor.Sum(nil), salt...) - keyComponent2 := hmac.New(sha1.New, compositeHash[:]) - keyComponent2.Write(hmacWithSalt) + k2 := hmac.New(sha1.New, ck[:]) + k2.Write(append(hmac1.Sum(nil), entrySalt...)) - key := append(keyComponent1.Sum(nil), keyComponent2.Sum(nil)...) - iv := key[len(key)-8:] - return key[:24], iv + dk := append(k1.Sum(nil), k2.Sum(nil)...) + return dk[:24], dk[len(dk)-8:] } // MetaPBE Struct @@ -107,17 +115,17 @@ func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { // OBJECT IDENTIFIER // OCTET STRING (14 byte) // OCTET STRING (16 byte) -type metaPBE struct { +type passwordCheckPBE struct { AlgoAttr algoAttr Encrypted []byte } type algoAttr struct { asn1.ObjectIdentifier - Data struct { - Data struct { + KDFParams struct { + PBKDF2 struct { asn1.ObjectIdentifier - SlatAttr slatAttr + SaltAttr saltAttr } IVData ivAttr } @@ -128,7 +136,7 @@ type ivAttr struct { IV []byte } -type slatAttr struct { +type saltAttr struct { EntrySalt []byte IterationCount int KeySize int @@ -137,81 +145,69 @@ type slatAttr struct { } } -func (m metaPBE) Decrypt(globalSalt []byte) ([]byte, error) { +func (m passwordCheckPBE) Decrypt(globalSalt []byte) ([]byte, error) { key, iv := m.deriveKeyAndIV(globalSalt) - - return AES128CBCDecrypt(key, iv, m.Encrypted) + return AESCBCDecrypt(key, iv, m.Encrypted) } -func (m metaPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) { +func (m passwordCheckPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) { key, iv := m.deriveKeyAndIV(globalSalt) - - return AES128CBCEncrypt(key, iv, plaintext) + return AESCBCEncrypt(key, iv, plaintext) } -func (m metaPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { +func (m passwordCheckPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { password := sha1.Sum(globalSalt) - salt := m.AlgoAttr.Data.Data.SlatAttr.EntrySalt - iter := m.AlgoAttr.Data.Data.SlatAttr.IterationCount - keyLen := m.AlgoAttr.Data.Data.SlatAttr.KeySize + params := m.AlgoAttr.KDFParams.PBKDF2.SaltAttr + key := PBKDF2Key(password[:], params.EntrySalt, params.IterationCount, params.KeySize, sha256.New) - key := PBKDF2Key(password[:], salt, iter, keyLen, sha256.New) - iv := append([]byte{4, 14}, m.AlgoAttr.Data.IVData.IV...) + // Firefox stores the IV with its ASN.1 OCTET STRING header (tag=0x04, length=0x0E). + // The full 16-byte IV = [0x04, 0x0E] + 14-byte IV value from the parsed structure. + iv := append([]byte{0x04, 0x0E}, m.AlgoAttr.KDFParams.IVData.IV...) return key, iv } -// loginPBE Struct +// credentialPBE Struct // // OCTET STRING (16 byte) // SEQUENCE (2 elem) // OBJECT IDENTIFIER // OCTET STRING (8 byte) // OCTET STRING (16 byte) -type loginPBE struct { - CipherText []byte - Data struct { +type credentialPBE struct { + KeyCheck []byte + Algo struct { asn1.ObjectIdentifier IV []byte } Encrypted []byte } -func (l loginPBE) Decrypt(globalSalt []byte) ([]byte, error) { - key, iv := l.deriveKeyAndIV(globalSalt) - // The encryption algorithm can be reliably inferred from IV length: - // - 8 bytes : 3DES-CBC (legacy Firefox versions) - // - 16 bytes : AES-CBC (Firefox 144+) - if len(iv) == 8 { - // Use 3DES for old Firefox versions - return DES3Decrypt(key[:24], iv, l.Encrypted) - } else if len(iv) == 16 { - // Firefox 144+ uses 32-byte keys (AES-256-CBC) - // Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths - return AES128CBCDecrypt(key, iv, l.Encrypted) +func (l credentialPBE) Decrypt(masterKey []byte) ([]byte, error) { + key, iv := l.deriveKeyAndIV(masterKey) + // The cipher is inferred from IV length (avoids fragile OID checks): + switch len(iv) { + case des.BlockSize: // 8: 3DES-CBC (legacy Firefox) + return DES3Decrypt(key[:des3KeySize], iv, l.Encrypted) + case aes.BlockSize: // 16: AES-256-CBC (Firefox 144+) + return AESCBCDecrypt(key, iv, l.Encrypted) + default: + return nil, errUnsupportedIVLen } - - return nil, errors.New("unsupported IV length for loginPBE decryption") } -func (l loginPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) { - key, iv := l.deriveKeyAndIV(globalSalt) - // The encryption algorithm can be reliably inferred from IV length: - // - 8 bytes : 3DES-CBC (legacy Firefox versions) - // - 16 bytes : AES-CBC (Firefox 144+) - // This avoids relying on NSS-specific OIDs, which have changed historically. - if len(iv) == 8 { - // Use 3DES for old Firefox versions - return DES3Encrypt(key[:24], iv, plaintext) - } else if len(iv) == 16 { - // Firefox 144+ uses 32-byte keys (AES-256-CBC) - // Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths - return AES128CBCEncrypt(key, iv, plaintext) +func (l credentialPBE) Encrypt(masterKey, plaintext []byte) ([]byte, error) { + key, iv := l.deriveKeyAndIV(masterKey) + switch len(iv) { + case des.BlockSize: + return DES3Encrypt(key[:des3KeySize], iv, plaintext) + case aes.BlockSize: + return AESCBCEncrypt(key, iv, plaintext) + default: + return nil, errUnsupportedIVLen } - - return nil, errors.New("unsupported IV length for loginPBE encryption") } -func (l loginPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) { - return globalSalt, l.Data.IV +func (l credentialPBE) deriveKeyAndIV(masterKey []byte) ([]byte, []byte) { + return masterKey, l.Algo.IV } diff --git a/crypto/asn1pbe_test.go b/crypto/asn1pbe_test.go index 0d802cb..31dbe91 100644 --- a/crypto/asn1pbe_test.go +++ b/crypto/asn1pbe_test.go @@ -11,18 +11,18 @@ import ( ) var ( - pbeIV = []byte("01234567") // 8 bytes - pbePlaintext = []byte("Hello, World!") - pbeCipherText = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1} - objWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3} - objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46} - objWithSHA1AndAES = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} - nssPBETestCases = []struct { + pbeIV = []byte("01234567") // 8 bytes + pbePlaintext = []byte("Hello, World!") + pbeKeyCheck = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1} + objWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3} + objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46} + objWithSHA1AndAES = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13} + privateKeyPBETestCases = []struct { RawHexPBE string GlobalSalt []byte Encrypted []byte IterationCount int - Len int + KeyLen int Plaintext []byte ObjectIdentifier asn1.ObjectIdentifier }{ @@ -32,11 +32,11 @@ var ( Encrypted: []byte{0x95, 0x18, 0x3a, 0x14, 0xc7, 0x52, 0xe7, 0xb1, 0xd0, 0xaa, 0xa4, 0x7f, 0x53, 0xe0, 0x50, 0x97}, Plaintext: pbePlaintext, IterationCount: 1, - Len: 32, + KeyLen: 32, ObjectIdentifier: objWithSHA1AndAES, }, } - metaPBETestCases = []struct { + passwordCheckPBETestCases = []struct { RawHexPBE string GlobalSalt []byte Encrypted []byte @@ -53,7 +53,7 @@ var ( ObjectIdentifier: objWithSHA256AndAES, }, } - loginPBETestCases = []struct { + credentialPBETestCases = []struct { RawHexPBE string GlobalSalt []byte Encrypted []byte @@ -73,108 +73,108 @@ var ( ) func TestNewASN1PBE(t *testing.T) { - for _, tc := range nssPBETestCases { + for _, tc := range privateKeyPBETestCases { nssRaw, err := hex.DecodeString(tc.RawHexPBE) require.NoError(t, err) pbe, err := NewASN1PBE(nssRaw) require.NoError(t, err) - nssPBETC, ok := pbe.(nssPBE) + privateKeyPBETC, ok := pbe.(privateKeyPBE) assert.True(t, ok) - assert.Equal(t, nssPBETC.Encrypted, tc.Encrypted) - assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt) - assert.Equal(t, 20, nssPBETC.AlgoAttr.SaltAttr.Len) - assert.Equal(t, nssPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier) + assert.Equal(t, privateKeyPBETC.Encrypted, tc.Encrypted) + assert.Equal(t, privateKeyPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt) + assert.Equal(t, 20, privateKeyPBETC.AlgoAttr.SaltAttr.KeyLen) + assert.Equal(t, privateKeyPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier) } } -func TestNssPBE_Encrypt(t *testing.T) { - for _, tc := range nssPBETestCases { - nssPBETC := nssPBE{ +func TestPrivateKeyPBE_Encrypt(t *testing.T) { + for _, tc := range privateKeyPBETestCases { + privateKeyPBETC := privateKeyPBE{ Encrypted: tc.Encrypted, AlgoAttr: struct { asn1.ObjectIdentifier SaltAttr struct { EntrySalt []byte - Len int + KeyLen int } }{ ObjectIdentifier: tc.ObjectIdentifier, SaltAttr: struct { EntrySalt []byte - Len int + KeyLen int }{ EntrySalt: tc.GlobalSalt, - Len: 20, + KeyLen: 20, }, }, } - encrypted, err := nssPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext) + encrypted, err := privateKeyPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext) require.NoError(t, err) assert.NotEmpty(t, encrypted) - assert.Equal(t, nssPBETC.Encrypted, encrypted) + assert.Equal(t, privateKeyPBETC.Encrypted, encrypted) } } -func TestNssPBE_Decrypt(t *testing.T) { - for _, tc := range nssPBETestCases { - nssPBETC := nssPBE{ +func TestPrivateKeyPBE_Decrypt(t *testing.T) { + for _, tc := range privateKeyPBETestCases { + privateKeyPBETC := privateKeyPBE{ Encrypted: tc.Encrypted, AlgoAttr: struct { asn1.ObjectIdentifier SaltAttr struct { EntrySalt []byte - Len int + KeyLen int } }{ ObjectIdentifier: tc.ObjectIdentifier, SaltAttr: struct { EntrySalt []byte - Len int + KeyLen int }{ EntrySalt: tc.GlobalSalt, - Len: 20, + KeyLen: 20, }, }, } - decrypted, err := nssPBETC.Decrypt(tc.GlobalSalt) + decrypted, err := privateKeyPBETC.Decrypt(tc.GlobalSalt) require.NoError(t, err) assert.NotEmpty(t, decrypted) assert.Equal(t, pbePlaintext, decrypted) } } -func TestNewASN1PBE_MetaPBE(t *testing.T) { - for _, tc := range metaPBETestCases { +func TestNewASN1PBE_PasswordCheckPBE(t *testing.T) { + for _, tc := range passwordCheckPBETestCases { metaRaw, err := hex.DecodeString(tc.RawHexPBE) require.NoError(t, err) pbe, err := NewASN1PBE(metaRaw) require.NoError(t, err) - metaPBETC, ok := pbe.(metaPBE) + passwordCheckPBETC, ok := pbe.(passwordCheckPBE) assert.True(t, ok) - assert.Equal(t, metaPBETC.Encrypted, tc.Encrypted) - assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.IV, tc.IV) - assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.ObjectIdentifier, objWithSHA256AndAES) + assert.Equal(t, passwordCheckPBETC.Encrypted, tc.Encrypted) + assert.Equal(t, passwordCheckPBETC.AlgoAttr.KDFParams.IVData.IV, tc.IV) + assert.Equal(t, passwordCheckPBETC.AlgoAttr.KDFParams.IVData.ObjectIdentifier, objWithSHA256AndAES) } } -func TestMetaPBE_Encrypt(t *testing.T) { - for _, tc := range metaPBETestCases { - metaPBETC := metaPBE{ +func TestPasswordCheckPBE_Encrypt(t *testing.T) { + for _, tc := range passwordCheckPBETestCases { + passwordCheckPBETC := passwordCheckPBE{ AlgoAttr: algoAttr{ ObjectIdentifier: tc.ObjectIdentifier, - Data: struct { - Data struct { + KDFParams: struct { + PBKDF2 struct { asn1.ObjectIdentifier - SlatAttr slatAttr + SaltAttr saltAttr } IVData ivAttr }{ - Data: struct { + PBKDF2: struct { asn1.ObjectIdentifier - SlatAttr slatAttr + SaltAttr saltAttr }{ ObjectIdentifier: tc.ObjectIdentifier, - SlatAttr: slatAttr{ + SaltAttr: saltAttr{ EntrySalt: tc.GlobalSalt, IterationCount: 1, KeySize: 32, @@ -193,31 +193,31 @@ func TestMetaPBE_Encrypt(t *testing.T) { }, Encrypted: tc.Encrypted, } - encrypted, err := metaPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext) + encrypted, err := passwordCheckPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext) require.NoError(t, err) assert.NotEmpty(t, encrypted) - assert.Equal(t, metaPBETC.Encrypted, encrypted) + assert.Equal(t, passwordCheckPBETC.Encrypted, encrypted) } } -func TestMetaPBE_Decrypt(t *testing.T) { - for _, tc := range metaPBETestCases { - metaPBETC := metaPBE{ +func TestPasswordCheckPBE_Decrypt(t *testing.T) { + for _, tc := range passwordCheckPBETestCases { + passwordCheckPBETC := passwordCheckPBE{ AlgoAttr: algoAttr{ ObjectIdentifier: tc.ObjectIdentifier, - Data: struct { - Data struct { + KDFParams: struct { + PBKDF2 struct { asn1.ObjectIdentifier - SlatAttr slatAttr + SaltAttr saltAttr } IVData ivAttr }{ - Data: struct { + PBKDF2: struct { asn1.ObjectIdentifier - SlatAttr slatAttr + SaltAttr saltAttr }{ ObjectIdentifier: tc.ObjectIdentifier, - SlatAttr: slatAttr{ + SaltAttr: saltAttr{ EntrySalt: tc.GlobalSalt, IterationCount: 1, KeySize: 32, @@ -236,32 +236,32 @@ func TestMetaPBE_Decrypt(t *testing.T) { }, Encrypted: tc.Encrypted, } - decrypted, err := metaPBETC.Decrypt(tc.GlobalSalt) + decrypted, err := passwordCheckPBETC.Decrypt(tc.GlobalSalt) require.NoError(t, err) assert.NotEmpty(t, decrypted) assert.Equal(t, pbePlaintext, decrypted) } } -func TestNewASN1PBE_LoginPBE(t *testing.T) { - for _, tc := range loginPBETestCases { +func TestNewASN1PBE_CredentialPBE(t *testing.T) { + for _, tc := range credentialPBETestCases { loginRaw, err := hex.DecodeString(tc.RawHexPBE) require.NoError(t, err) pbe, err := NewASN1PBE(loginRaw) require.NoError(t, err) - loginPBETC, ok := pbe.(loginPBE) + credentialPBETC, ok := pbe.(credentialPBE) assert.True(t, ok) - assert.Equal(t, loginPBETC.Encrypted, tc.Encrypted) - assert.Equal(t, loginPBETC.Data.IV, tc.IV) - assert.Equal(t, loginPBETC.Data.ObjectIdentifier, objWithMD5AndDESCBC) + assert.Equal(t, credentialPBETC.Encrypted, tc.Encrypted) + assert.Equal(t, credentialPBETC.Algo.IV, tc.IV) + assert.Equal(t, credentialPBETC.Algo.ObjectIdentifier, objWithMD5AndDESCBC) } } -func TestLoginPBE_Encrypt(t *testing.T) { - for _, tc := range loginPBETestCases { - loginPBETC := loginPBE{ - CipherText: pbeCipherText, - Data: struct { +func TestCredentialPBE_Encrypt(t *testing.T) { + for _, tc := range credentialPBETestCases { + credentialPBETC := credentialPBE{ + KeyCheck: pbeKeyCheck, + Algo: struct { asn1.ObjectIdentifier IV []byte }{ @@ -270,18 +270,18 @@ func TestLoginPBE_Encrypt(t *testing.T) { }, Encrypted: tc.Encrypted, } - encrypted, err := loginPBETC.Encrypt(tc.GlobalSalt, plainText) + encrypted, err := credentialPBETC.Encrypt(tc.GlobalSalt, plainText) require.NoError(t, err) assert.NotEmpty(t, encrypted) - assert.Equal(t, loginPBETC.Encrypted, encrypted) + assert.Equal(t, credentialPBETC.Encrypted, encrypted) } } -func TestLoginPBE_Decrypt(t *testing.T) { - for _, tc := range loginPBETestCases { - loginPBETC := loginPBE{ - CipherText: pbeCipherText, - Data: struct { +func TestCredentialPBE_Decrypt(t *testing.T) { + for _, tc := range credentialPBETestCases { + credentialPBETC := credentialPBE{ + KeyCheck: pbeKeyCheck, + Algo: struct { asn1.ObjectIdentifier IV []byte }{ @@ -290,9 +290,63 @@ func TestLoginPBE_Decrypt(t *testing.T) { }, Encrypted: tc.Encrypted, } - decrypted, err := loginPBETC.Decrypt(tc.GlobalSalt) + decrypted, err := credentialPBETC.Decrypt(tc.GlobalSalt) require.NoError(t, err) assert.NotEmpty(t, decrypted) assert.Equal(t, pbePlaintext, decrypted) } } + +func TestNewASN1PBE_InvalidData(t *testing.T) { + _, err := NewASN1PBE([]byte{0xFF, 0xFF}) + assert.ErrorIs(t, err, errDecodeASN1) +} + +func TestCredentialPBE_AES256CBC(t *testing.T) { + // Test the Firefox 144+ AES-256-CBC path (IV length = 16). + // Construct a credentialPBE with a 16-byte IV to exercise the AES branch. + masterKey := bytes.Repeat([]byte("k"), 32) // AES-256 key + iv := bytes.Repeat([]byte{0x01}, 16) // 16-byte IV → AES-CBC path + + // Encrypt plaintext to get valid ciphertext for round-trip test. + encrypted, err := AESCBCEncrypt(masterKey, iv, pbePlaintext) + require.NoError(t, err) + + pbe := credentialPBE{ + KeyCheck: pbeKeyCheck, + Algo: struct { + asn1.ObjectIdentifier + IV []byte + }{ + ObjectIdentifier: objWithSHA256AndAES, + IV: iv, + }, + Encrypted: encrypted, + } + + decrypted, err := pbe.Decrypt(masterKey) + require.NoError(t, err) + assert.Equal(t, pbePlaintext, decrypted) + + // Verify encrypt round-trip + reEncrypted, err := pbe.Encrypt(masterKey, pbePlaintext) + require.NoError(t, err) + assert.Equal(t, encrypted, reEncrypted) +} + +func TestCredentialPBE_UnsupportedIVLength(t *testing.T) { + pbe := credentialPBE{ + Algo: struct { + asn1.ObjectIdentifier + IV []byte + }{ + IV: []byte{1, 2, 3}, // 3-byte IV: neither 8 nor 16 + }, + Encrypted: []byte("data"), + } + _, err := pbe.Decrypt([]byte("key")) + require.ErrorIs(t, err, errUnsupportedIVLen) + + _, err = pbe.Encrypt([]byte("key"), []byte("data")) + require.ErrorIs(t, err, errUnsupportedIVLen) +} diff --git a/crypto/crypto.go b/crypto/crypto.go index ab32c2b..3244300 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -1,161 +1,154 @@ package crypto import ( - "bytes" "crypto/aes" "crypto/cipher" "crypto/des" - "errors" "fmt" ) -var ErrCiphertextLengthIsInvalid = errors.New("ciphertext length is invalid") - -// AES128CBCDecrypt decrypts data using AES-CBC mode. -// Note: Despite the function name, this supports all AES key sizes. -// The Go standard library's aes.NewCipher automatically selects the AES variant -// based on the key length: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256). -// TODO: Rename to AESCBCDecrypt to avoid confusion about supported key lengths. -func AES128CBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) { +// AESCBCEncrypt encrypts data using AES-CBC mode with PKCS5 padding. +// Supports all AES key sizes: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256). +func AESCBCEncrypt(key, iv, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - // Check ciphertext length - if len(ciphertext) < aes.BlockSize { - return nil, errors.New("AES128CBCDecrypt: ciphertext too short") - } - if len(ciphertext)%aes.BlockSize != 0 { - return nil, errors.New("AES128CBCDecrypt: ciphertext is not a multiple of the block size") - } - - decryptedData := make([]byte, len(ciphertext)) - mode := cipher.NewCBCDecrypter(block, iv) - mode.CryptBlocks(decryptedData, ciphertext) - - // unpad the decrypted data and handle potential padding errors - decryptedData, err = pkcs5UnPadding(decryptedData) - if err != nil { - return nil, fmt.Errorf("AES128CBCDecrypt: %w", err) - } - - return decryptedData, nil + return cbcEncrypt(block, iv, plaintext) } -func AES128CBCEncrypt(key, iv, plaintext []byte) ([]byte, error) { +// AESCBCDecrypt decrypts data using AES-CBC mode with PKCS5 unpadding. +// Supports all AES key sizes: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256). +func AESCBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - - if len(iv) != aes.BlockSize { - return nil, errors.New("AES128CBCEncrypt: iv length is invalid, must equal block size") - } - - plaintext = pkcs5Padding(plaintext, block.BlockSize()) - encryptedData := make([]byte, len(plaintext)) - mode := cipher.NewCBCEncrypter(block, iv) - mode.CryptBlocks(encryptedData, plaintext) - - return encryptedData, nil -} - -func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) { - block, err := des.NewTripleDESCipher(key) - if err != nil { - return nil, err - } - if len(ciphertext) < des.BlockSize { - return nil, errors.New("DES3Decrypt: ciphertext too short") - } - if len(ciphertext)%block.BlockSize() != 0 { - return nil, errors.New("DES3Decrypt: ciphertext is not a multiple of the block size") - } - - blockMode := cipher.NewCBCDecrypter(block, iv) - sq := make([]byte, len(ciphertext)) - blockMode.CryptBlocks(sq, ciphertext) - - return pkcs5UnPadding(sq) + return cbcDecrypt(block, iv, ciphertext) } +// DES3Encrypt encrypts data using 3DES-CBC mode with PKCS5 padding. func DES3Encrypt(key, iv, plaintext []byte) ([]byte, error) { block, err := des.NewTripleDESCipher(key) if err != nil { return nil, err } - - plaintext = pkcs5Padding(plaintext, block.BlockSize()) - dst := make([]byte, len(plaintext)) - blockMode := cipher.NewCBCEncrypter(block, iv) - blockMode.CryptBlocks(dst, plaintext) - - return dst, nil + return cbcEncrypt(block, iv, plaintext) } -// AESGCMDecrypt chromium > 80 https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/sync/os_crypt_win.cc -func AESGCMDecrypt(key, nounce, ciphertext []byte) ([]byte, error) { - block, err := aes.NewCipher(key) +// DES3Decrypt decrypts data using 3DES-CBC mode with PKCS5 unpadding. +func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) { + block, err := des.NewTripleDESCipher(key) if err != nil { return nil, err } - blockMode, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - origData, err := blockMode.Open(nil, nounce, ciphertext, nil) - if err != nil { - return nil, err - } - return origData, nil + return cbcDecrypt(block, iv, ciphertext) } -// AESGCMEncrypt encrypts plaintext using AES encryption in GCM mode. +// AESGCMEncrypt encrypts data using AES-GCM mode. func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - blockMode, err := cipher.NewGCM(block) + aead, err := cipher.NewGCM(block) if err != nil { return nil, err } - // The first parameter is the prefix for the output, we can leave it nil. - // The Seal method encrypts and authenticates the data, appending the result to the dst. - encryptedData := blockMode.Seal(nil, nonce, plaintext, nil) - return encryptedData, nil + if len(nonce) != aead.NonceSize() { + return nil, errInvalidNonceLen + } + return aead.Seal(nil, nonce, plaintext, nil), nil } +// AESGCMDecrypt decrypts data using AES-GCM mode. +func AESGCMDecrypt(key, nonce, ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + if len(nonce) != aead.NonceSize() { + return nil, errInvalidNonceLen + } + return aead.Open(nil, nonce, ciphertext, nil) +} + +// cbcEncrypt adds PKCS5 padding and encrypts plaintext in CBC mode. +func cbcEncrypt(block cipher.Block, iv, plaintext []byte) ([]byte, error) { + if len(iv) != block.BlockSize() { + return nil, errInvalidIVLength + } + + padded := pkcs5Padding(plaintext, block.BlockSize()) + dst := make([]byte, len(padded)) + cipher.NewCBCEncrypter(block, iv).CryptBlocks(dst, padded) + return dst, nil +} + +// cbcDecrypt decrypts ciphertext in CBC mode and removes PKCS5 padding. +func cbcDecrypt(block cipher.Block, iv, ciphertext []byte) ([]byte, error) { + bs := block.BlockSize() + if len(iv) != bs { + return nil, errInvalidIVLength + } + if len(ciphertext) < bs { + return nil, errShortCiphertext + } + if len(ciphertext)%bs != 0 { + return nil, errInvalidBlockSize + } + + dst := make([]byte, len(ciphertext)) + cipher.NewCBCDecrypter(block, iv).CryptBlocks(dst, ciphertext) + + dst, err := pkcs5UnPadding(dst, bs) + if err != nil { + return nil, fmt.Errorf("decrypt: %w", err) + } + return dst, nil +} + +// paddingZero pads src with zero bytes to the given length. +// Returns src unchanged if already long enough; otherwise returns a new slice. func paddingZero(src []byte, length int) []byte { - padding := length - len(src) - if padding <= 0 { + if len(src) >= length { return src } - return append(src, make([]byte, padding)...) + dst := make([]byte, length) + copy(dst, src) + return dst } -func pkcs5UnPadding(src []byte) ([]byte, error) { +// pkcs5Padding adds PKCS5/PKCS7 padding to src. +// Always returns a new slice; never modifies src. +func pkcs5Padding(src []byte, blockSize int) []byte { + n := blockSize - (len(src) % blockSize) + dst := make([]byte, len(src)+n) + copy(dst, src) + for i := len(src); i < len(dst); i++ { + dst[i] = byte(n) + } + return dst +} + +// pkcs5UnPadding removes PKCS5/PKCS7 padding from src. +func pkcs5UnPadding(src []byte, blockSize int) ([]byte, error) { length := len(src) if length == 0 { - return nil, errors.New("pkcs5UnPadding: src should not be empty") + return nil, errInvalidPadding } padding := int(src[length-1]) - if padding < 1 || padding > aes.BlockSize { - return nil, errors.New("pkcs5UnPadding: invalid padding size") - } - if padding > length { - return nil, errors.New("pkcs5UnPadding: invalid padding length") + if padding < 1 || padding > blockSize || padding > length { + return nil, errInvalidPadding } for _, b := range src[length-padding:] { if int(b) != padding { - return nil, errors.New("pkcs5UnPadding: invalid padding content") + return nil, errInvalidPadding } } return src[:length-padding], nil } - -func pkcs5Padding(src []byte, blocksize int) []byte { - padding := blocksize - (len(src) % blocksize) - padText := bytes.Repeat([]byte{byte(padding)}, padding) - return append(src, padText...) -} diff --git a/crypto/crypto_darwin.go b/crypto/crypto_darwin.go index b128271..c739daa 100644 --- a/crypto/crypto_darwin.go +++ b/crypto/crypto_darwin.go @@ -2,18 +2,22 @@ package crypto -import "errors" +import ( + "bytes" + "crypto/aes" +) -var ErrDarwinNotSupportDPAPI = errors.New("darwin not support dpapi") +var chromiumCBCIV = bytes.Repeat([]byte{0x20}, aes.BlockSize) -func DecryptWithChromium(key, password []byte) ([]byte, error) { - if len(password) <= 3 { - return nil, ErrCiphertextLengthIsInvalid +const minCBCDataSize = versionPrefixLen + aes.BlockSize // "v10" + one AES block = 19 bytes minimum + +func DecryptChromium(key, ciphertext []byte) ([]byte, error) { + if len(ciphertext) < minCBCDataSize { + return nil, errShortCiphertext } - iv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32} - return AES128CBCDecrypt(key, iv, password[3:]) + return AESCBCDecrypt(key, chromiumCBCIV, ciphertext[versionPrefixLen:]) } -func DecryptWithDPAPI(_ []byte) ([]byte, error) { - return nil, ErrDarwinNotSupportDPAPI +func DecryptDPAPI(_ []byte) ([]byte, error) { + return nil, errDPAPINotSupported } diff --git a/crypto/crypto_linux.go b/crypto/crypto_linux.go index d9eaf0e..78c152c 100644 --- a/crypto/crypto_linux.go +++ b/crypto/crypto_linux.go @@ -2,14 +2,22 @@ package crypto -func DecryptWithChromium(key, encryptPass []byte) ([]byte, error) { - if len(encryptPass) < 3 { - return nil, ErrCiphertextLengthIsInvalid +import ( + "bytes" + "crypto/aes" +) + +var chromiumCBCIV = bytes.Repeat([]byte{0x20}, aes.BlockSize) + +const minCBCDataSize = versionPrefixLen + aes.BlockSize // "v10" + one AES block = 19 bytes minimum + +func DecryptChromium(key, ciphertext []byte) ([]byte, error) { + if len(ciphertext) < minCBCDataSize { + return nil, errShortCiphertext } - iv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32} - return AES128CBCDecrypt(key, iv, encryptPass[3:]) + return AESCBCDecrypt(key, chromiumCBCIV, ciphertext[versionPrefixLen:]) } -func DecryptWithDPAPI(_ []byte) ([]byte, error) { - return nil, nil +func DecryptDPAPI(_ []byte) ([]byte, error) { + return nil, errDPAPINotSupported } diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index 805466a..249d446 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -27,16 +27,16 @@ var ( aesGCMCiphertext = "6c49dac89992639713edab3a114c450968a08b53556872cea3919e2e9a" ) -func TestAES128CBCEncrypt(t *testing.T) { - encrypted, err := AES128CBCEncrypt(aesKey, aesIV, plainText) +func TestAESCBCEncrypt(t *testing.T) { + encrypted, err := AESCBCEncrypt(aesKey, aesIV, plainText) require.NoError(t, err) assert.NotEmpty(t, encrypted) assert.Equal(t, aes128Ciphertext, fmt.Sprintf("%x", encrypted)) } -func TestAES128CBCDecrypt(t *testing.T) { +func TestAESCBCDecrypt(t *testing.T) { ciphertext, _ := hex.DecodeString(aes128Ciphertext) - decrypted, err := AES128CBCDecrypt(aesKey, aesIV, ciphertext) + decrypted, err := AESCBCDecrypt(aesKey, aesIV, ciphertext) require.NoError(t, err) assert.NotEmpty(t, decrypted) assert.Equal(t, plainText, decrypted) @@ -71,3 +71,117 @@ func TestAESGCMDecrypt(t *testing.T) { assert.NotEmpty(t, decrypted) assert.Equal(t, plainText, decrypted) } + +// --- Bug-fix verification tests --- +// These tests verify the fixes for known issues in the crypto primitives. +// Tests marked with t.Skip document bugs that exist before the fix. + +func TestPkcs5Padding_NoMutation(t *testing.T) { + // pkcs5Padding should not mutate the original slice's backing array. + src := make([]byte, 3, 32) // len=3, cap=32 — append won't allocate + copy(src, "abc") + backup := make([]byte, cap(src)) + copy(backup, src[:cap(src)]) + + padded := pkcs5Padding(src, 16) + assert.Len(t, padded, 16) + assert.Equal(t, []byte("abc"), src) // original length unchanged + + // The bytes beyond len(src) in the original backing array must not be touched. + current := make([]byte, cap(src)) + copy(current, src[:cap(src)]) + assert.Equal(t, backup, current, "pkcs5Padding mutated the original slice backing array") +} + +func TestPaddingZero_NoMutation(t *testing.T) { + src := make([]byte, 3, 32) + copy(src, "abc") + backup := make([]byte, cap(src)) + copy(backup, src[:cap(src)]) + + padded := paddingZero(src, 20) + assert.Len(t, padded, 20) + + current := make([]byte, cap(src)) + copy(current, src[:cap(src)]) + assert.Equal(t, backup, current, "paddingZero mutated the original slice backing array") +} + +func TestAESCBCDecrypt_WrongIVLength(t *testing.T) { + key := bytes.Repeat([]byte("k"), 16) + wrongIV := []byte("short") + ct := make([]byte, 16) + + _, err := AESCBCDecrypt(key, wrongIV, ct) + require.Error(t, err, "wrong IV length should return error, not panic") + assert.ErrorIs(t, err, errInvalidIVLength) +} + +func TestAESCBCEncrypt_WrongIVLength(t *testing.T) { + key := bytes.Repeat([]byte("k"), 16) + wrongIV := []byte("short") + + _, err := AESCBCEncrypt(key, wrongIV, plainText) + require.Error(t, err) + assert.ErrorIs(t, err, errInvalidIVLength) +} + +func TestDES3Decrypt_WrongIVLength(t *testing.T) { + key := bytes.Repeat([]byte("k"), 24) + wrongIV := []byte("ab") // DES needs 8-byte IV + ct := make([]byte, 8) + + _, err := DES3Decrypt(key, wrongIV, ct) + require.Error(t, err, "wrong IV length should return error, not panic") + assert.ErrorIs(t, err, errInvalidIVLength) +} + +func TestDES3Encrypt_WrongIVLength(t *testing.T) { + key := bytes.Repeat([]byte("k"), 24) + wrongIV := []byte("ab") + + _, err := DES3Encrypt(key, wrongIV, plainText) + require.Error(t, err, "wrong IV length should return error, not panic") + assert.ErrorIs(t, err, errInvalidIVLength) +} + +func TestAESCBCDecrypt_EmptyCiphertext(t *testing.T) { + key := bytes.Repeat([]byte("k"), 16) + iv := bytes.Repeat([]byte("i"), 16) + + _, err := AESCBCDecrypt(key, iv, nil) + require.Error(t, err) + + _, err = AESCBCDecrypt(key, iv, []byte{}) + require.Error(t, err) +} + +func TestAESGCMEncrypt_WrongNonceLength(t *testing.T) { + key := bytes.Repeat([]byte("k"), 16) + wrongNonce := []byte("short") + + _, err := AESGCMEncrypt(key, wrongNonce, plainText) + require.Error(t, err, "wrong nonce length should return error, not panic") + assert.ErrorIs(t, err, errInvalidNonceLen) +} + +func TestAESGCMDecrypt_WrongNonceLength(t *testing.T) { + key := bytes.Repeat([]byte("k"), 16) + wrongNonce := []byte("short") + ct := make([]byte, 32) + + _, err := AESGCMDecrypt(key, wrongNonce, ct) + require.Error(t, err, "wrong nonce length should return error, not panic") + assert.ErrorIs(t, err, errInvalidNonceLen) +} + +func TestDES3Decrypt_EmptyCiphertext(t *testing.T) { + key := bytes.Repeat([]byte("k"), 24) + iv := bytes.Repeat([]byte("i"), 8) + + _, err := DES3Decrypt(key, iv, nil) + require.Error(t, err) + + _, err = DES3Decrypt(key, iv, []byte{}) + require.Error(t, err) +} diff --git a/crypto/crypto_windows.go b/crypto/crypto_windows.go index fdf5b15..5f50ba6 100644 --- a/crypto/crypto_windows.go +++ b/crypto/crypto_windows.go @@ -9,35 +9,29 @@ import ( ) const ( - // Assuming the nonce size is 12 bytes and the minimum encrypted data size is 3 bytes - minEncryptedDataSize = 15 - nonceSize = 12 + gcmNonceSize = 12 // AES-GCM standard nonce size + minGCMDataSize = versionPrefixLen + gcmNonceSize // "v10" + nonce = 15 bytes minimum ) -func DecryptWithChromium(key, ciphertext []byte) ([]byte, error) { - if len(ciphertext) < minEncryptedDataSize { - return nil, ErrCiphertextLengthIsInvalid +func DecryptChromium(key, ciphertext []byte) ([]byte, error) { + if len(ciphertext) < minGCMDataSize { + return nil, errShortCiphertext } - - nonce := ciphertext[3 : 3+nonceSize] - encryptedPassword := ciphertext[3+nonceSize:] - - return AESGCMDecrypt(key, nonce, encryptedPassword) + nonce := ciphertext[versionPrefixLen : versionPrefixLen+gcmNonceSize] + payload := ciphertext[versionPrefixLen+gcmNonceSize:] + return AESGCMDecrypt(key, nonce, payload) } -// DecryptWithYandex decrypts the password with AES-GCM -func DecryptWithYandex(key, ciphertext []byte) ([]byte, error) { - if len(ciphertext) < minEncryptedDataSize { - return nil, ErrCiphertextLengthIsInvalid +// DecryptYandex decrypts a Yandex-encrypted value. +// TODO: Yandex uses the same AES-GCM format as Chromium for now; +// update when Yandex-specific decryption diverges. +func DecryptYandex(key, ciphertext []byte) ([]byte, error) { + if len(ciphertext) < minGCMDataSize { + return nil, errShortCiphertext } - // remove Prefix 'v10' - // gcmBlockSize = 16 - // gcmTagSize = 16 - // gcmMinimumTagSize = 12 // NIST SP 800-38D recommends tags with 12 or more bytes. - // gcmStandardNonceSize = 12 - nonce := ciphertext[3 : 3+nonceSize] - encryptedPassword := ciphertext[3+nonceSize:] - return AESGCMDecrypt(key, nonce, encryptedPassword) + nonce := ciphertext[versionPrefixLen : versionPrefixLen+gcmNonceSize] + payload := ciphertext[versionPrefixLen+gcmNonceSize:] + return AESGCMDecrypt(key, nonce, payload) } type dataBlob struct { @@ -61,11 +55,11 @@ func (b *dataBlob) bytes() []byte { return d } -// DecryptWithDPAPI (Data Protection Application Programming Interface) +// DecryptDPAPI (Data Protection Application Programming Interface) // is a simple cryptographic application programming interface // available as a built-in component in Windows 2000 and // later versions of Microsoft Windows operating systems -func DecryptWithDPAPI(ciphertext []byte) ([]byte, error) { +func DecryptDPAPI(ciphertext []byte) ([]byte, error) { crypt32 := syscall.NewLazyDLL("Crypt32.dll") kernel32 := syscall.NewLazyDLL("Kernel32.dll") unprotectDataProc := crypt32.NewProc("CryptUnprotectData") diff --git a/crypto/errors.go b/crypto/errors.go new file mode 100644 index 0000000..955accb --- /dev/null +++ b/crypto/errors.go @@ -0,0 +1,15 @@ +package crypto + +import "errors" + +// Sentinel errors for crypto operations. +var ( + errShortCiphertext = errors.New("ciphertext too short") + errInvalidBlockSize = errors.New("ciphertext is not a multiple of the block size") + errInvalidIVLength = errors.New("IV length must equal block size") + errInvalidPadding = errors.New("invalid PKCS5 padding") + errInvalidNonceLen = errors.New("nonce length must equal GCM nonce size") + errUnsupportedIVLen = errors.New("unsupported IV length") + errDecodeASN1 = errors.New("failed to decode ASN1 data") + errDPAPINotSupported = errors.New("DPAPI not supported on this platform") //nolint:unused // used on darwin/linux only +) diff --git a/crypto/keyretriever/gcoredump_darwin.go b/crypto/keyretriever/gcoredump_darwin.go index f18e961..8c05e05 100644 --- a/crypto/keyretriever/gcoredump_darwin.go +++ b/crypto/keyretriever/gcoredump_darwin.go @@ -69,7 +69,7 @@ type addressRange struct { // DecryptKeychain extracts the browser storage password from login.keychain-db // by dumping securityd memory and scanning for the keychain master key. // Requires root privileges. -func DecryptKeychain(storagename string) (string, error) { +func DecryptKeychain(storageName string) (string, error) { if os.Geteuid() != 0 { return "", errors.New("requires root privileges") } @@ -124,13 +124,13 @@ func DecryptKeychain(storagename string) (string, error) { continue } for _, rec := range records { - if rec.Account == storagename { + if rec.Account == storageName { return string(rec.Password), nil } } } - return "", fmt.Errorf("tried %d candidates, none matched storage %q", len(candidates), storagename) + return "", fmt.Errorf("tried %d candidates, none matched storage %q", len(candidates), storageName) } // scanMasterKeyCandidates scans the core dump for 24-byte master key candidates. diff --git a/crypto/keyretriever/keyretriever_darwin.go b/crypto/keyretriever/keyretriever_darwin.go index a0881a7..3720563 100644 --- a/crypto/keyretriever/keyretriever_darwin.go +++ b/crypto/keyretriever/keyretriever_darwin.go @@ -20,7 +20,7 @@ import ( var darwinParams = pbkdf2Params{ salt: []byte("saltysalt"), iterations: 1003, - keyLen: 16, + keySize: 16, hashFunc: sha1.New, } diff --git a/crypto/keyretriever/keyretriever_linux.go b/crypto/keyretriever/keyretriever_linux.go index 25ff49a..dc4463f 100644 --- a/crypto/keyretriever/keyretriever_linux.go +++ b/crypto/keyretriever/keyretriever_linux.go @@ -14,7 +14,7 @@ import ( var linuxParams = pbkdf2Params{ salt: []byte("saltysalt"), iterations: 1, - keyLen: 16, + keySize: 16, hashFunc: sha1.New, } diff --git a/crypto/keyretriever/keyretriever_windows.go b/crypto/keyretriever/keyretriever_windows.go index d660369..619aa2f 100644 --- a/crypto/keyretriever/keyretriever_windows.go +++ b/crypto/keyretriever/keyretriever_windows.go @@ -41,7 +41,7 @@ func (r *DPAPIRetriever) RetrieveKey(_, localStatePath string) ([]byte, error) { return nil, fmt.Errorf("encrypted_key unexpected prefix: got %q, want %q", keyBytes[:len(dpapiPrefix)], dpapiPrefix) } - masterKey, err := crypto.DecryptWithDPAPI(keyBytes[len(dpapiPrefix):]) + masterKey, err := crypto.DecryptDPAPI(keyBytes[len(dpapiPrefix):]) if err != nil { return nil, fmt.Errorf("DPAPI decrypt: %w", err) } diff --git a/crypto/keyretriever/params.go b/crypto/keyretriever/params.go index 66e33e8..b90f1cc 100644 --- a/crypto/keyretriever/params.go +++ b/crypto/keyretriever/params.go @@ -13,11 +13,11 @@ import ( type pbkdf2Params struct { salt []byte iterations int - keyLen int + keySize int hashFunc func() hash.Hash } // deriveKey derives an encryption key from a secret using PBKDF2. func (p pbkdf2Params) deriveKey(secret []byte) []byte { - return crypto.PBKDF2Key(secret, p.salt, p.iterations, p.keyLen, p.hashFunc) + return crypto.PBKDF2Key(secret, p.salt, p.iterations, p.keySize, p.hashFunc) } diff --git a/crypto/pbkdf2.go b/crypto/pbkdf2.go index 2e016df..56ad1b9 100644 --- a/crypto/pbkdf2.go +++ b/crypto/pbkdf2.go @@ -22,7 +22,7 @@ import ( // Using a higher iteration count will increase the cost of an exhaustive // search but will also make derivation proportionally slower. // Copy from https://golang.org/x/crypto/pbkdf2 -func PBKDF2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte { +func PBKDF2Key(password, salt []byte, iterations, keyLen int, h func() hash.Hash) []byte { prf := hmac.New(h, password) hashLen := prf.Size() numBlocks := (keyLen + hashLen - 1) / hashLen @@ -45,7 +45,7 @@ func PBKDF2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []by t := dk[len(dk)-hashLen:] copy(u, t) - for n := 2; n <= iter; n++ { + for n := 2; n <= iterations; n++ { prf.Reset() prf.Write(u) u = u[:0] diff --git a/crypto/pbkdf2_test.go b/crypto/pbkdf2_test.go new file mode 100644 index 0000000..dc30060 --- /dev/null +++ b/crypto/pbkdf2_test.go @@ -0,0 +1,61 @@ +package crypto + +import ( + "crypto/sha1" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test vectors from RFC 6070 (PKCS #5 PBKDF2 with HMAC-SHA1). +// https://www.rfc-editor.org/rfc/rfc6070 +func TestPBKDF2Key_RFC6070(t *testing.T) { + tests := []struct { + name string + password string + salt string + iterations int + keyLen int + want string + }{ + { + name: "iteration=1", + password: "password", + salt: "salt", + iterations: 1, + keyLen: 20, + want: "0c60c80f961f0e71f3a9b524af6012062fe037a6", + }, + { + name: "iteration=2", + password: "password", + salt: "salt", + iterations: 2, + keyLen: 20, + want: "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957", + }, + { + name: "iteration=4096", + password: "password", + salt: "salt", + iterations: 4096, + keyLen: 20, + want: "4b007901b765489abead49d926f721d065a429c1", + }, + { + name: "long_password_and_salt", + password: "passwordPASSWORDpassword", + salt: "saltSALTsaltSALTsaltSALTsaltSALTsalt", + iterations: 4096, + keyLen: 25, + want: "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := PBKDF2Key([]byte(tt.password), []byte(tt.salt), tt.iterations, tt.keyLen, sha1.New) + assert.Equal(t, tt.want, hex.EncodeToString(got)) + }) + } +} diff --git a/crypto/version.go b/crypto/version.go index dbb055a..ac74734 100644 --- a/crypto/version.go +++ b/crypto/version.go @@ -12,14 +12,17 @@ const ( // CipherDPAPI is pre-Chrome 80 raw DPAPI encryption (no version prefix). CipherDPAPI CipherVersion = "dpapi" + + // versionPrefixLen is the byte length of the version prefix ("v10", "v20"). + versionPrefixLen = 3 ) // DetectVersion identifies the encryption version from a ciphertext prefix. func DetectVersion(ciphertext []byte) CipherVersion { - if len(ciphertext) < 3 { + if len(ciphertext) < versionPrefixLen { return CipherDPAPI } - prefix := string(ciphertext[:3]) + prefix := string(ciphertext[:versionPrefixLen]) switch prefix { case "v10": return CipherV10 @@ -30,12 +33,12 @@ func DetectVersion(ciphertext []byte) CipherVersion { } } -// StripPrefix removes the version prefix (e.g. "v10") from ciphertext. +// stripPrefix removes the version prefix (e.g. "v10") from ciphertext. // Returns the ciphertext unchanged if no known prefix is found. -func StripPrefix(ciphertext []byte) []byte { +func stripPrefix(ciphertext []byte) []byte { ver := DetectVersion(ciphertext) if ver == CipherV10 || ver == CipherV20 { - return ciphertext[3:] + return ciphertext[versionPrefixLen:] } return ciphertext } diff --git a/crypto/version_test.go b/crypto/version_test.go index efd7c0c..74afbee 100644 --- a/crypto/version_test.go +++ b/crypto/version_test.go @@ -27,7 +27,7 @@ func TestDetectVersion(t *testing.T) { } } -func TestStripPrefix(t *testing.T) { +func Test_stripPrefix(t *testing.T) { tests := []struct { name string ciphertext []byte @@ -41,7 +41,7 @@ func TestStripPrefix(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, StripPrefix(tt.ciphertext)) + assert.Equal(t, tt.want, stripPrefix(tt.ciphertext)) }) } } diff --git a/filemanager/copy_windows.go b/filemanager/copy_windows.go index f6010b2..073de9c 100644 --- a/filemanager/copy_windows.go +++ b/filemanager/copy_windows.go @@ -20,7 +20,7 @@ const ( statusInfoLengthMismatch = 0xC0000004 - fileMAPRead = 0x0004 + fileMapRead = 0x0004 pageReadonly = 0x02 fileTypeDisk = 0x0001 ) @@ -288,7 +288,7 @@ func readViaFileMapping(handle windows.Handle, size int) ([]byte, error) { defer windows.CloseHandle(windows.Handle(mapping)) viewPtr, _, err := procMapViewOfFile.Call( - mapping, fileMAPRead, + mapping, fileMapRead, 0, 0, 0, ) if viewPtr == 0 { diff --git a/types/category.go b/types/category.go index 7edabdd..6c2c600 100644 --- a/types/category.go +++ b/types/category.go @@ -74,10 +74,10 @@ func NonSensitiveCategories() []Category { type BrowserKind int const ( - KindChromium BrowserKind = iota - KindChromiumYandex // Chromium variant with different file names and extract logic - KindChromiumOpera // Opera: extensions in "opsettings" key, data in Roaming - KindFirefox + Chromium BrowserKind = iota + ChromiumYandex // Chromium variant with different file names and extract logic + ChromiumOpera // Opera: extensions in "opsettings" key, data in Roaming + Firefox ) // BrowserConfig holds the declarative configuration for a browser installation. diff --git a/utils/fileutil/filetutil.go b/utils/fileutil/fileutil.go similarity index 95% rename from utils/fileutil/filetutil.go rename to utils/fileutil/fileutil.go index 3ea2e55..f21ab5a 100644 --- a/utils/fileutil/filetutil.go +++ b/utils/fileutil/fileutil.go @@ -8,8 +8,8 @@ import ( "path/filepath" ) -// IsFileExists checks if the file exists in the provided path -func IsFileExists(filename string) bool { +// FileExists checks if the file exists in the provided path. +func FileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false diff --git a/utils/typeutil/typeutil.go b/utils/typeutil/typeutil.go deleted file mode 100644 index 58b13ff..0000000 --- a/utils/typeutil/typeutil.go +++ /dev/null @@ -1,34 +0,0 @@ -package typeutil - -import ( - "time" -) - -func Reverse[T any](s []T) []T { - h := make([]T, len(s)) - for i := 0; i < len(s); i++ { - h[i] = s[len(s)-i-1] - } - return h -} - -func TimeStamp(stamp int64) time.Time { - s := time.Unix(stamp, 0) - if s.Local().Year() > 9999 { - return time.Date(9999, 12, 13, 23, 59, 59, 0, time.Local) - } - return s -} - -func TimeEpoch(epoch int64) time.Time { - maxTime := int64(99633311740000000) - if epoch > maxTime { - return time.Date(2049, 1, 1, 1, 1, 1, 1, time.Local) - } - t := time.Date(1601, 1, 1, 0, 0, 0, 0, time.Local) - d := time.Duration(epoch) - for i := 0; i < 1000; i++ { - t = t.Add(d) - } - return t -} diff --git a/utils/typeutil/typeutil_test.go b/utils/typeutil/typeutil_test.go deleted file mode 100644 index 68e0aeb..0000000 --- a/utils/typeutil/typeutil_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package typeutil - -import ( - "testing" -) - -func TestReverse(t *testing.T) { - t.Parallel() - - reverseTestCases := [][]any{ - {1, 2, 3, 4, 5}, - {"1", "2", "3", "4", "5"}, - {"1", 2, "3", "4", 5}, - } - - for _, ts := range reverseTestCases { - h := Reverse(ts) - for i := 0; i < len(ts); i++ { - if h[len(h)-i-1] != ts[i] { - t.Errorf("reverse failed %v != %v", h, ts) - } - } - } -}