mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-07-08 21:37:49 +02:00
refactor: naming cleanup and crypto package improvements (#551)
* refactor: naming cleanup across all packages
This commit is contained in:
@@ -5,5 +5,6 @@ Sie = "Sie"
|
|||||||
OT = "OT"
|
OT = "OT"
|
||||||
Encrypter = "Encrypter"
|
Encrypter = "Encrypter"
|
||||||
Decrypter = "Decrypter"
|
Decrypter = "Decrypter"
|
||||||
|
PASSWOR = "PASSWOR"
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = ["go.mod", "go.sum"]
|
extend-exclude = ["go.mod", "go.sum"]
|
||||||
+3
-3
@@ -48,7 +48,7 @@ func pickFromConfigs(configs []types.BrowserConfig, opts PickOptions) ([]Browser
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.ProfilePath != "" && name != "all" {
|
if opts.ProfilePath != "" && name != "all" {
|
||||||
if cfg.Kind == types.KindFirefox {
|
if cfg.Kind == types.Firefox {
|
||||||
cfg.UserDataDir = filepath.Dir(filepath.Clean(opts.ProfilePath))
|
cfg.UserDataDir = filepath.Dir(filepath.Clean(opts.ProfilePath))
|
||||||
} else {
|
} else {
|
||||||
cfg.UserDataDir = opts.ProfilePath
|
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.
|
// newBrowsers dispatches to the correct engine based on BrowserKind.
|
||||||
func newBrowsers(cfg types.BrowserConfig) ([]Browser, error) {
|
func newBrowsers(cfg types.BrowserConfig) ([]Browser, error) {
|
||||||
switch cfg.Kind {
|
switch cfg.Kind {
|
||||||
case types.KindChromium, types.KindChromiumYandex, types.KindChromiumOpera:
|
case types.Chromium, types.ChromiumYandex, types.ChromiumOpera:
|
||||||
bs, err := chromium.NewBrowsers(cfg)
|
bs, err := chromium.NewBrowsers(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -87,7 +87,7 @@ func newBrowsers(cfg types.BrowserConfig) ([]Browser, error) {
|
|||||||
}
|
}
|
||||||
return browsers, nil
|
return browsers, nil
|
||||||
|
|
||||||
case types.KindFirefox:
|
case types.Firefox:
|
||||||
bs, err := firefox.NewBrowsers(cfg)
|
bs, err := firefox.NewBrowsers(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+12
-12
@@ -11,84 +11,84 @@ func platformBrowsers() []types.BrowserConfig {
|
|||||||
{
|
{
|
||||||
Key: "chrome",
|
Key: "chrome",
|
||||||
Name: chromeName,
|
Name: chromeName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Chrome",
|
Storage: "Chrome",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Google/Chrome",
|
UserDataDir: homeDir + "/Library/Application Support/Google/Chrome",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "edge",
|
Key: "edge",
|
||||||
Name: edgeName,
|
Name: edgeName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Microsoft Edge",
|
Storage: "Microsoft Edge",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Microsoft Edge",
|
UserDataDir: homeDir + "/Library/Application Support/Microsoft Edge",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "chromium",
|
Key: "chromium",
|
||||||
Name: chromiumName,
|
Name: chromiumName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Chromium",
|
Storage: "Chromium",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Chromium",
|
UserDataDir: homeDir + "/Library/Application Support/Chromium",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "chrome-beta",
|
Key: "chrome-beta",
|
||||||
Name: chromeBetaName,
|
Name: chromeBetaName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Chrome",
|
Storage: "Chrome",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Google/Chrome Beta",
|
UserDataDir: homeDir + "/Library/Application Support/Google/Chrome Beta",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "opera",
|
Key: "opera",
|
||||||
Name: operaName,
|
Name: operaName,
|
||||||
Kind: types.KindChromiumOpera,
|
Kind: types.ChromiumOpera,
|
||||||
Storage: "Opera",
|
Storage: "Opera",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.Opera",
|
UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.Opera",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "opera-gx",
|
Key: "opera-gx",
|
||||||
Name: operaGXName,
|
Name: operaGXName,
|
||||||
Kind: types.KindChromiumOpera,
|
Kind: types.ChromiumOpera,
|
||||||
Storage: "Opera",
|
Storage: "Opera",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.OperaGX",
|
UserDataDir: homeDir + "/Library/Application Support/com.operasoftware.OperaGX",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "vivaldi",
|
Key: "vivaldi",
|
||||||
Name: vivaldiName,
|
Name: vivaldiName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Vivaldi",
|
Storage: "Vivaldi",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Vivaldi",
|
UserDataDir: homeDir + "/Library/Application Support/Vivaldi",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "coccoc",
|
Key: "coccoc",
|
||||||
Name: coccocName,
|
Name: coccocName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "CocCoc",
|
Storage: "CocCoc",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Coccoc",
|
UserDataDir: homeDir + "/Library/Application Support/Coccoc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "brave",
|
Key: "brave",
|
||||||
Name: braveName,
|
Name: braveName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Brave",
|
Storage: "Brave",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/BraveSoftware/Brave-Browser",
|
UserDataDir: homeDir + "/Library/Application Support/BraveSoftware/Brave-Browser",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "yandex",
|
Key: "yandex",
|
||||||
Name: yandexName,
|
Name: yandexName,
|
||||||
Kind: types.KindChromiumYandex,
|
Kind: types.ChromiumYandex,
|
||||||
Storage: "Yandex",
|
Storage: "Yandex",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Yandex/YandexBrowser",
|
UserDataDir: homeDir + "/Library/Application Support/Yandex/YandexBrowser",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "arc",
|
Key: "arc",
|
||||||
Name: arcName,
|
Name: arcName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Arc",
|
Storage: "Arc",
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Arc/User Data",
|
UserDataDir: homeDir + "/Library/Application Support/Arc/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "firefox",
|
Key: "firefox",
|
||||||
Name: firefoxName,
|
Name: firefoxName,
|
||||||
Kind: types.KindFirefox,
|
Kind: types.Firefox,
|
||||||
UserDataDir: homeDir + "/Library/Application Support/Firefox/Profiles",
|
UserDataDir: homeDir + "/Library/Application Support/Firefox/Profiles",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,56 +11,56 @@ func platformBrowsers() []types.BrowserConfig {
|
|||||||
{
|
{
|
||||||
Key: "chrome",
|
Key: "chrome",
|
||||||
Name: chromeName,
|
Name: chromeName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Chrome Safe Storage",
|
Storage: "Chrome Safe Storage",
|
||||||
UserDataDir: homeDir + "/.config/google-chrome",
|
UserDataDir: homeDir + "/.config/google-chrome",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "edge",
|
Key: "edge",
|
||||||
Name: edgeName,
|
Name: edgeName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Chromium Safe Storage",
|
Storage: "Chromium Safe Storage",
|
||||||
UserDataDir: homeDir + "/.config/microsoft-edge",
|
UserDataDir: homeDir + "/.config/microsoft-edge",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "chromium",
|
Key: "chromium",
|
||||||
Name: chromiumName,
|
Name: chromiumName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Chromium Safe Storage",
|
Storage: "Chromium Safe Storage",
|
||||||
UserDataDir: homeDir + "/.config/chromium",
|
UserDataDir: homeDir + "/.config/chromium",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "chrome-beta",
|
Key: "chrome-beta",
|
||||||
Name: chromeBetaName,
|
Name: chromeBetaName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Chrome Safe Storage",
|
Storage: "Chrome Safe Storage",
|
||||||
UserDataDir: homeDir + "/.config/google-chrome-beta",
|
UserDataDir: homeDir + "/.config/google-chrome-beta",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "opera",
|
Key: "opera",
|
||||||
Name: operaName,
|
Name: operaName,
|
||||||
Kind: types.KindChromiumOpera,
|
Kind: types.ChromiumOpera,
|
||||||
Storage: "Chromium Safe Storage",
|
Storage: "Chromium Safe Storage",
|
||||||
UserDataDir: homeDir + "/.config/opera",
|
UserDataDir: homeDir + "/.config/opera",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "vivaldi",
|
Key: "vivaldi",
|
||||||
Name: vivaldiName,
|
Name: vivaldiName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Chrome Safe Storage",
|
Storage: "Chrome Safe Storage",
|
||||||
UserDataDir: homeDir + "/.config/vivaldi",
|
UserDataDir: homeDir + "/.config/vivaldi",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "brave",
|
Key: "brave",
|
||||||
Name: braveName,
|
Name: braveName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
Storage: "Brave Safe Storage",
|
Storage: "Brave Safe Storage",
|
||||||
UserDataDir: homeDir + "/.config/BraveSoftware/Brave-Browser",
|
UserDataDir: homeDir + "/.config/BraveSoftware/Brave-Browser",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "firefox",
|
Key: "firefox",
|
||||||
Name: firefoxName,
|
Name: firefoxName,
|
||||||
Kind: types.KindFirefox,
|
Kind: types.Firefox,
|
||||||
UserDataDir: homeDir + "/.mozilla/firefox",
|
UserDataDir: homeDir + "/.mozilla/firefox",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ func TestPickFromConfigs_NameFilter(t *testing.T) {
|
|||||||
mkFile(t, dir, "Default", "History")
|
mkFile(t, dir, "Default", "History")
|
||||||
|
|
||||||
configs := []types.BrowserConfig{
|
configs := []types.BrowserConfig{
|
||||||
{Key: "chrome", Name: "Chrome", Kind: types.KindChromium, UserDataDir: dir},
|
{Key: "chrome", Name: "Chrome", Kind: types.Chromium, UserDataDir: dir},
|
||||||
{Key: "edge", Name: "Edge", Kind: types.KindChromium, UserDataDir: dir},
|
{Key: "edge", Name: "Edge", Kind: types.Chromium, UserDataDir: dir},
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -102,7 +102,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "chromium multi-profile",
|
name: "chromium multi-profile",
|
||||||
configs: []types.BrowserConfig{
|
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"},
|
wantNames: []string{"Chrome", "Chrome"},
|
||||||
wantProfiles: []string{"Default", "Profile 1"},
|
wantProfiles: []string{"Default", "Profile 1"},
|
||||||
@@ -110,7 +110,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "firefox random dir",
|
name: "firefox random dir",
|
||||||
configs: []types.BrowserConfig{
|
configs: []types.BrowserConfig{
|
||||||
{Key: "firefox", Name: "Firefox", Kind: types.KindFirefox, UserDataDir: firefoxDir},
|
{Key: "firefox", Name: "Firefox", Kind: types.Firefox, UserDataDir: firefoxDir},
|
||||||
},
|
},
|
||||||
wantNames: []string{"Firefox"},
|
wantNames: []string{"Firefox"},
|
||||||
wantProfiles: []string{"abc123.default-release"},
|
wantProfiles: []string{"abc123.default-release"},
|
||||||
@@ -118,7 +118,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "yandex variant",
|
name: "yandex variant",
|
||||||
configs: []types.BrowserConfig{
|
configs: []types.BrowserConfig{
|
||||||
{Key: "yandex", Name: "Yandex", Kind: types.KindChromiumYandex, UserDataDir: yandexDir},
|
{Key: "yandex", Name: "Yandex", Kind: types.ChromiumYandex, UserDataDir: yandexDir},
|
||||||
},
|
},
|
||||||
wantNames: []string{"Yandex"},
|
wantNames: []string{"Yandex"},
|
||||||
wantProfiles: []string{"Default"},
|
wantProfiles: []string{"Default"},
|
||||||
@@ -126,7 +126,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "nonexistent dir",
|
name: "nonexistent dir",
|
||||||
configs: []types.BrowserConfig{
|
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",
|
name: "chromium uses path directly",
|
||||||
configs: []types.BrowserConfig{
|
configs: []types.BrowserConfig{
|
||||||
{Key: "chrome", Name: "Chrome", Kind: types.KindChromium, UserDataDir: "/wrong"},
|
{Key: "chrome", Name: "Chrome", Kind: types.Chromium, UserDataDir: "/wrong"},
|
||||||
},
|
},
|
||||||
pickName: "chrome",
|
pickName: "chrome",
|
||||||
profilePath: filepath.Join(chromeDir, "Default"),
|
profilePath: filepath.Join(chromeDir, "Default"),
|
||||||
@@ -174,7 +174,7 @@ func TestPickFromConfigs_ProfilePath(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "firefox uses parent dir",
|
name: "firefox uses parent dir",
|
||||||
configs: []types.BrowserConfig{
|
configs: []types.BrowserConfig{
|
||||||
{Key: "firefox", Name: "Firefox", Kind: types.KindFirefox, UserDataDir: "/wrong"},
|
{Key: "firefox", Name: "Firefox", Kind: types.Firefox, UserDataDir: "/wrong"},
|
||||||
},
|
},
|
||||||
pickName: "firefox",
|
pickName: "firefox",
|
||||||
profilePath: filepath.Join(firefoxDir, "abc123.default-release"),
|
profilePath: filepath.Join(firefoxDir, "abc123.default-release"),
|
||||||
@@ -184,7 +184,7 @@ func TestPickFromConfigs_ProfilePath(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "ignored when name is all",
|
name: "ignored when name is all",
|
||||||
configs: []types.BrowserConfig{
|
configs: []types.BrowserConfig{
|
||||||
{Key: "chrome", Name: "Chrome", Kind: types.KindChromium, UserDataDir: chromeDir},
|
{Key: "chrome", Name: "Chrome", Kind: types.Chromium, UserDataDir: chromeDir},
|
||||||
},
|
},
|
||||||
pickName: "all",
|
pickName: "all",
|
||||||
profilePath: "/some/override",
|
profilePath: "/some/override",
|
||||||
|
|||||||
+18
-18
@@ -11,97 +11,97 @@ func platformBrowsers() []types.BrowserConfig {
|
|||||||
{
|
{
|
||||||
Key: "chrome",
|
Key: "chrome",
|
||||||
Name: chromeName,
|
Name: chromeName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/Google/Chrome/User Data",
|
UserDataDir: homeDir + "/AppData/Local/Google/Chrome/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "edge",
|
Key: "edge",
|
||||||
Name: edgeName,
|
Name: edgeName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/Microsoft/Edge/User Data",
|
UserDataDir: homeDir + "/AppData/Local/Microsoft/Edge/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "chromium",
|
Key: "chromium",
|
||||||
Name: chromiumName,
|
Name: chromiumName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/Chromium/User Data",
|
UserDataDir: homeDir + "/AppData/Local/Chromium/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "chrome-beta",
|
Key: "chrome-beta",
|
||||||
Name: chromeBetaName,
|
Name: chromeBetaName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/Google/Chrome Beta/User Data",
|
UserDataDir: homeDir + "/AppData/Local/Google/Chrome Beta/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "opera",
|
Key: "opera",
|
||||||
Name: operaName,
|
Name: operaName,
|
||||||
Kind: types.KindChromiumOpera,
|
Kind: types.ChromiumOpera,
|
||||||
UserDataDir: homeDir + "/AppData/Roaming/Opera Software/Opera Stable",
|
UserDataDir: homeDir + "/AppData/Roaming/Opera Software/Opera Stable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "opera-gx",
|
Key: "opera-gx",
|
||||||
Name: operaGXName,
|
Name: operaGXName,
|
||||||
Kind: types.KindChromiumOpera,
|
Kind: types.ChromiumOpera,
|
||||||
UserDataDir: homeDir + "/AppData/Roaming/Opera Software/Opera GX Stable",
|
UserDataDir: homeDir + "/AppData/Roaming/Opera Software/Opera GX Stable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "vivaldi",
|
Key: "vivaldi",
|
||||||
Name: vivaldiName,
|
Name: vivaldiName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/Vivaldi/User Data",
|
UserDataDir: homeDir + "/AppData/Local/Vivaldi/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "coccoc",
|
Key: "coccoc",
|
||||||
Name: coccocName,
|
Name: coccocName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/CocCoc/Browser/User Data",
|
UserDataDir: homeDir + "/AppData/Local/CocCoc/Browser/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "brave",
|
Key: "brave",
|
||||||
Name: braveName,
|
Name: braveName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/BraveSoftware/Brave-Browser/User Data",
|
UserDataDir: homeDir + "/AppData/Local/BraveSoftware/Brave-Browser/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "yandex",
|
Key: "yandex",
|
||||||
Name: yandexName,
|
Name: yandexName,
|
||||||
Kind: types.KindChromiumYandex,
|
Kind: types.ChromiumYandex,
|
||||||
UserDataDir: homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data",
|
UserDataDir: homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "360x",
|
Key: "360x",
|
||||||
Name: speed360XName,
|
Name: speed360XName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/360ChromeX/Chrome/User Data",
|
UserDataDir: homeDir + "/AppData/Local/360ChromeX/Chrome/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "360",
|
Key: "360",
|
||||||
Name: speed360Name,
|
Name: speed360Name,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/360chrome/Chrome/User Data",
|
UserDataDir: homeDir + "/AppData/Local/360chrome/Chrome/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "qq",
|
Key: "qq",
|
||||||
Name: qqBrowserName,
|
Name: qqName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/Tencent/QQBrowser/User Data",
|
UserDataDir: homeDir + "/AppData/Local/Tencent/QQBrowser/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "dc",
|
Key: "dc",
|
||||||
Name: dcBrowserName,
|
Name: dcName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/DCBrowser/User Data",
|
UserDataDir: homeDir + "/AppData/Local/DCBrowser/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "sogou",
|
Key: "sogou",
|
||||||
Name: sogouName,
|
Name: sogouName,
|
||||||
Kind: types.KindChromium,
|
Kind: types.Chromium,
|
||||||
UserDataDir: homeDir + "/AppData/Local/Sogou/SogouExplorer/User Data",
|
UserDataDir: homeDir + "/AppData/Local/Sogou/SogouExplorer/User Data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "firefox",
|
Key: "firefox",
|
||||||
Name: firefoxName,
|
Name: firefoxName,
|
||||||
Kind: types.KindFirefox,
|
Kind: types.Firefox,
|
||||||
UserDataDir: homeDir + "/AppData/Roaming/Mozilla/Firefox/Profiles",
|
UserDataDir: homeDir + "/AppData/Roaming/Mozilla/Firefox/Profiles",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package chromium
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
|
||||||
"github.com/moond4rk/hackbrowserdata/filemanager"
|
"github.com/moond4rk/hackbrowserdata/filemanager"
|
||||||
@@ -125,7 +126,7 @@ func (b *Browser) getMasterKey(session *filemanager.Session) ([]byte, error) {
|
|||||||
var localStateDst string
|
var localStateDst string
|
||||||
for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} {
|
for _, dir := range []string{filepath.Dir(b.profileDir), b.profileDir} {
|
||||||
candidate := filepath.Join(dir, "Local State")
|
candidate := filepath.Join(dir, "Local State")
|
||||||
if fileutil.IsFileExists(candidate) {
|
if fileutil.FileExists(candidate) {
|
||||||
localStateDst = filepath.Join(session.TempDir(), "Local State")
|
localStateDst = filepath.Join(session.TempDir(), "Local State")
|
||||||
if err := session.Acquire(candidate, localStateDst, false); err != nil {
|
if err := session.Acquire(candidate, localStateDst, false); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -273,3 +274,18 @@ func isSkippedDir(name string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func TestNewBrowsers(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "chrome multi-profile",
|
name: "chrome multi-profile",
|
||||||
dir: fixture.chrome,
|
dir: fixture.chrome,
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
wantProfiles: []string{"Default", "Profile 1", "Profile 3"},
|
wantProfiles: []string{"Default", "Profile 1", "Profile 3"},
|
||||||
wantCats: map[string][]string{
|
wantCats: map[string][]string{
|
||||||
"Default": {"Login Data", "Cookies", "History", "Bookmarks", "Web Data", "Secure Preferences", "leveldb", "Session Storage"},
|
"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",
|
name: "opera with Default",
|
||||||
dir: fixture.opera,
|
dir: fixture.opera,
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
wantProfiles: []string{"Default"},
|
wantProfiles: []string{"Default"},
|
||||||
wantCats: map[string][]string{
|
wantCats: map[string][]string{
|
||||||
"Default": {"Login Data", "History", "Bookmarks", "Cookies"},
|
"Default": {"Login Data", "History", "Bookmarks", "Cookies"},
|
||||||
@@ -162,7 +162,7 @@ func TestNewBrowsers(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "opera flat layout",
|
name: "opera flat layout",
|
||||||
dir: fixture.operaFlat,
|
dir: fixture.operaFlat,
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
wantProfiles: []string{filepath.Base(fixture.operaFlat)}, // userDataDir itself
|
wantProfiles: []string{filepath.Base(fixture.operaFlat)}, // userDataDir itself
|
||||||
wantCats: map[string][]string{
|
wantCats: map[string][]string{
|
||||||
filepath.Base(fixture.operaFlat): {"Login Data", "History", "Cookies"},
|
filepath.Base(fixture.operaFlat): {"Login Data", "History", "Cookies"},
|
||||||
@@ -171,7 +171,7 @@ func TestNewBrowsers(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "yandex custom files",
|
name: "yandex custom files",
|
||||||
dir: fixture.yandex,
|
dir: fixture.yandex,
|
||||||
kind: types.KindChromiumYandex,
|
kind: types.ChromiumYandex,
|
||||||
wantProfiles: []string{"Default"},
|
wantProfiles: []string{"Default"},
|
||||||
wantCats: map[string][]string{
|
wantCats: map[string][]string{
|
||||||
"Default": {"Ya Passman Data", "Ya Credit Cards", "History", "Cookies", "Bookmarks"},
|
"Default": {"Ya Passman Data", "Ya Credit Cards", "History", "Cookies", "Bookmarks"},
|
||||||
@@ -180,38 +180,38 @@ func TestNewBrowsers(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "old cookies fallback",
|
name: "old cookies fallback",
|
||||||
dir: fixture.oldCookies,
|
dir: fixture.oldCookies,
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
wantProfiles: []string{"Default"},
|
wantProfiles: []string{"Default"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cookie priority",
|
name: "cookie priority",
|
||||||
dir: fixture.bothCookies,
|
dir: fixture.bothCookies,
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
wantProfiles: []string{"Default"},
|
wantProfiles: []string{"Default"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "leveldb directories",
|
name: "leveldb directories",
|
||||||
dir: fixture.leveldb,
|
dir: fixture.leveldb,
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
wantProfiles: []string{"Default"},
|
wantProfiles: []string{"Default"},
|
||||||
wantDirs: []types.Category{types.LocalStorage, types.SessionStorage},
|
wantDirs: []types.Category{types.LocalStorage, types.SessionStorage},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "leveldb only",
|
name: "leveldb only",
|
||||||
dir: fixture.leveldbOnly,
|
dir: fixture.leveldbOnly,
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
wantProfiles: []string{"Default"},
|
wantProfiles: []string{"Default"},
|
||||||
wantDirs: []types.Category{types.LocalStorage, types.SessionStorage},
|
wantDirs: []types.Category{types.LocalStorage, types.SessionStorage},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty dir",
|
name: "empty dir",
|
||||||
dir: fixture.empty,
|
dir: fixture.empty,
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nonexistent dir",
|
name: "nonexistent dir",
|
||||||
dir: "/nonexistent/path",
|
dir: "/nonexistent/path",
|
||||||
kind: types.KindChromium,
|
kind: types.Chromium,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,8 +321,8 @@ func TestSharedSourceFile(t *testing.T) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func TestSourcesForKind(t *testing.T) {
|
func TestSourcesForKind(t *testing.T) {
|
||||||
chromium := sourcesForKind(types.KindChromium)
|
chromium := sourcesForKind(types.Chromium)
|
||||||
yandex := sourcesForKind(types.KindChromiumYandex)
|
yandex := sourcesForKind(types.ChromiumYandex)
|
||||||
|
|
||||||
assert.Equal(t, "Login Data", chromium[types.Password][0].rel)
|
assert.Equal(t, "Login Data", chromium[types.Password][0].rel)
|
||||||
assert.Equal(t, "Ya Passman Data", yandex[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) {
|
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)
|
require.NotNil(t, yandexExt)
|
||||||
assert.Contains(t, yandexExt, types.Password)
|
assert.Contains(t, yandexExt, types.Password)
|
||||||
|
|
||||||
operaExt := extractorsForKind(types.KindChromiumOpera)
|
operaExt := extractorsForKind(types.ChromiumOpera)
|
||||||
require.NotNil(t, operaExt)
|
require.NotNil(t, operaExt)
|
||||||
assert.Contains(t, operaExt, types.Extension)
|
assert.Contains(t, operaExt, types.Extension)
|
||||||
}
|
}
|
||||||
@@ -425,7 +425,7 @@ func TestLocalStatePath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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.NoError(t, err)
|
||||||
require.NotEmpty(t, browsers)
|
require.NotEmpty(t, browsers)
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ func decryptValue(masterKey, ciphertext []byte) ([]byte, error) {
|
|||||||
version := crypto.DetectVersion(ciphertext)
|
version := crypto.DetectVersion(ciphertext)
|
||||||
switch version {
|
switch version {
|
||||||
case crypto.CipherV10:
|
case crypto.CipherV10:
|
||||||
return crypto.DecryptWithChromium(masterKey, ciphertext)
|
return crypto.DecryptChromium(masterKey, ciphertext)
|
||||||
case crypto.CipherV20:
|
case crypto.CipherV20:
|
||||||
// TODO: implement App-Bound Encryption (Chrome 127+)
|
// TODO: implement App-Bound Encryption (Chrome 127+)
|
||||||
return nil, fmt.Errorf("v20 App-Bound Encryption not yet supported")
|
return nil, fmt.Errorf("v20 App-Bound Encryption not yet supported")
|
||||||
case crypto.CipherDPAPI:
|
case crypto.CipherDPAPI:
|
||||||
return crypto.DecryptWithDPAPI(ciphertext)
|
return crypto.DecryptDPAPI(ciphertext)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported cipher version: %s", version)
|
return nil, fmt.Errorf("unsupported cipher version: %s", version)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,12 @@ import (
|
|||||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
"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) {
|
func TestDecryptValue_V10(t *testing.T) {
|
||||||
plaintext := []byte("test_secret_value")
|
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)
|
require.NoError(t, err)
|
||||||
v10Ciphertext := append([]byte("v10"), encrypted...)
|
v10Ciphertext := append([]byte("v10"), cbcEncrypted...)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -35,7 +33,7 @@ func TestDecryptValue_V10(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "wrong key returns padding error",
|
name: "wrong key returns padding error",
|
||||||
key: []byte("wrong_key_1234!!"),
|
key: []byte("wrong_key_1234!!"),
|
||||||
wantErrMsg: "pkcs5UnPadding",
|
wantErrMsg: "invalid PKCS5 padding",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// encryptWithDPAPI encrypts data using Windows DPAPI (CryptProtectData).
|
// 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) {
|
func encryptWithDPAPI(plaintext []byte) ([]byte, error) {
|
||||||
crypt32 := syscall.NewLazyDLL("Crypt32.dll")
|
crypt32 := syscall.NewLazyDLL("Crypt32.dll")
|
||||||
kernel32 := syscall.NewLazyDLL("Kernel32.dll")
|
kernel32 := syscall.NewLazyDLL("Kernel32.dll")
|
||||||
@@ -57,11 +57,11 @@ func TestDecryptValue_V10_Windows(t *testing.T) {
|
|||||||
plaintext := []byte("test_secret_value")
|
plaintext := []byte("test_secret_value")
|
||||||
nonce := []byte("123456789012") // 12-byte nonce
|
nonce := []byte("123456789012") // 12-byte nonce
|
||||||
|
|
||||||
encrypted, err := crypto.AESGCMEncrypt(testAESKey, nonce, plaintext)
|
gcmEncrypted, err := crypto.AESGCMEncrypt(testAESKey, nonce, plaintext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// v10 format on Windows: "v10" + nonce(12) + encrypted
|
// 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)
|
got, err := decryptValue(testAESKey, ciphertext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func extractBookmarks(path string) ([]types.BookmarkEntry, error) {
|
func extractBookmarks(path string) ([]types.BookmarkEntry, error) {
|
||||||
@@ -39,7 +38,7 @@ func walkBookmarks(node gjson.Result, folder string, out *[]types.BookmarkEntry)
|
|||||||
Type: nodeType,
|
Type: nodeType,
|
||||||
URL: node.Get("url").String(),
|
URL: node.Get("url").String(),
|
||||||
Folder: folder,
|
Folder: folder,
|
||||||
CreatedAt: typeutil.TimeEpoch(node.Get("date_added").Int()),
|
CreatedAt: timeEpoch(node.Get("date_added").Int()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/moond4rk/hackbrowserdata/log"
|
"github.com/moond4rk/hackbrowserdata/log"
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultCookieQuery = `SELECT name, encrypted_value, host_key, path,
|
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,
|
IsHTTPOnly: isHTTPOnly != 0,
|
||||||
HasExpire: hasExpire != 0,
|
HasExpire: hasExpire != 0,
|
||||||
IsPersistent: isPersistent != 0,
|
IsPersistent: isPersistent != 0,
|
||||||
ExpireAt: typeutil.TimeEpoch(expireAt),
|
ExpireAt: timeEpoch(expireAt),
|
||||||
CreatedAt: typeutil.TimeEpoch(createdAt),
|
CreatedAt: timeEpoch(createdAt),
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const defaultCreditCardQuery = `SELECT COALESCE(guid, ''), name_on_card, expirat
|
|||||||
func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry, error) {
|
func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry, error) {
|
||||||
return sqliteutil.QueryRows(path, false, defaultCreditCardQuery,
|
return sqliteutil.QueryRows(path, false, defaultCreditCardQuery,
|
||||||
func(rows *sql.Rows) (types.CreditCardEntry, error) {
|
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
|
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
|
return types.CreditCardEntry{}, err
|
||||||
}
|
}
|
||||||
number, err := decryptValue(masterKey, encNumber)
|
number, err := decryptValue(masterKey, encNumber)
|
||||||
@@ -29,7 +29,7 @@ func extractCreditCards(masterKey []byte, path string) ([]types.CreditCardEntry,
|
|||||||
Number: string(number),
|
Number: string(number),
|
||||||
ExpMonth: month,
|
ExpMonth: month,
|
||||||
ExpYear: year,
|
ExpYear: year,
|
||||||
NickName: nickName,
|
NickName: nickname,
|
||||||
Address: address,
|
Address: address,
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"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,
|
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,
|
TargetPath: targetPath,
|
||||||
MimeType: mimeType,
|
MimeType: mimeType,
|
||||||
TotalBytes: totalBytes,
|
TotalBytes: totalBytes,
|
||||||
StartTime: typeutil.TimeEpoch(startTime),
|
StartTime: timeEpoch(startTime),
|
||||||
EndTime: typeutil.TimeEpoch(endTime),
|
EndTime: timeEpoch(endTime),
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultHistoryQuery = `SELECT url, title, visit_count, last_visit_time FROM urls`
|
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,
|
URL: url,
|
||||||
Title: title,
|
Title: title,
|
||||||
VisitCount: visitCount,
|
VisitCount: visitCount,
|
||||||
LastVisit: typeutil.TimeEpoch(lastVisit),
|
LastVisit: timeEpoch(lastVisit),
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/moond4rk/hackbrowserdata/log"
|
"github.com/moond4rk/hackbrowserdata/log"
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"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`
|
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,
|
URL: url,
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: string(password),
|
Password: string(password),
|
||||||
CreatedAt: typeutil.TimeEpoch(created),
|
CreatedAt: timeEpoch(created),
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func yandexSources() map[types.Category][]sourcePath {
|
|||||||
// sourcesForKind returns the source mapping for a browser kind.
|
// sourcesForKind returns the source mapping for a browser kind.
|
||||||
func sourcesForKind(kind types.BrowserKind) map[types.Category][]sourcePath {
|
func sourcesForKind(kind types.BrowserKind) map[types.Category][]sourcePath {
|
||||||
switch kind {
|
switch kind {
|
||||||
case types.KindChromiumYandex:
|
case types.ChromiumYandex:
|
||||||
return yandexSources()
|
return yandexSources()
|
||||||
default:
|
default:
|
||||||
return chromiumSources
|
return chromiumSources
|
||||||
@@ -109,9 +109,9 @@ var operaExtractors = map[types.Category]categoryExtractor{
|
|||||||
// nil means all categories use the default extractCategory switch logic.
|
// nil means all categories use the default extractCategory switch logic.
|
||||||
func extractorsForKind(kind types.BrowserKind) map[types.Category]categoryExtractor {
|
func extractorsForKind(kind types.BrowserKind) map[types.Category]categoryExtractor {
|
||||||
switch kind {
|
switch kind {
|
||||||
case types.KindChromiumYandex:
|
case types.ChromiumYandex:
|
||||||
return yandexExtractors
|
return yandexExtractors
|
||||||
case types.KindChromiumOpera:
|
case types.ChromiumOpera:
|
||||||
return operaExtractors
|
return operaExtractors
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+2
-2
@@ -21,8 +21,8 @@ const (
|
|||||||
firefoxName = "Firefox"
|
firefoxName = "Firefox"
|
||||||
speed360Name = "360 Speed"
|
speed360Name = "360 Speed"
|
||||||
speed360XName = "360 Speed X"
|
speed360XName = "360 Speed X"
|
||||||
qqBrowserName = "QQ"
|
qqName = "QQ"
|
||||||
dcBrowserName = "DC"
|
dcName = "DC"
|
||||||
sogouName = "Sogou"
|
sogouName = "Sogou"
|
||||||
arcName = "Arc"
|
arcName = "Arc"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const firefoxBookmarkQuery = `SELECT id, url, type, dateAdded, COALESCE(title, '')
|
const firefoxBookmarkQuery = `SELECT id, url, type, dateAdded, COALESCE(title, '')
|
||||||
@@ -25,7 +24,7 @@ func extractBookmarks(path string) ([]types.BookmarkEntry, error) {
|
|||||||
Name: title,
|
Name: title,
|
||||||
URL: url,
|
URL: url,
|
||||||
Folder: bookmarkType(bt),
|
Folder: bookmarkType(bt),
|
||||||
CreatedAt: typeutil.TimeStamp(dateAdded / 1000000),
|
CreatedAt: timestamp(dateAdded / 1000000),
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const firefoxCookieQuery = `SELECT name, value, host, path,
|
const firefoxCookieQuery = `SELECT name, value, host, path,
|
||||||
@@ -34,8 +33,8 @@ func extractCookies(path string) ([]types.CookieEntry, error) {
|
|||||||
IsHTTPOnly: isHTTPOnly != 0,
|
IsHTTPOnly: isHTTPOnly != 0,
|
||||||
HasExpire: hasExpire,
|
HasExpire: hasExpire,
|
||||||
IsPersistent: hasExpire,
|
IsPersistent: hasExpire,
|
||||||
ExpireAt: typeutil.TimeStamp(expiry),
|
ExpireAt: timestamp(expiry),
|
||||||
CreatedAt: typeutil.TimeStamp(createdAt / 1000000),
|
CreatedAt: timestamp(createdAt / 1000000),
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const firefoxDownloadQuery = `SELECT place_id, GROUP_CONCAT(content), url, dateAdded
|
const firefoxDownloadQuery = `SELECT place_id, GROUP_CONCAT(content), url, dateAdded
|
||||||
@@ -27,7 +26,7 @@ func extractDownloads(path string) ([]types.DownloadEntry, error) {
|
|||||||
|
|
||||||
entry := types.DownloadEntry{
|
entry := types.DownloadEntry{
|
||||||
URL: url,
|
URL: url,
|
||||||
StartTime: typeutil.TimeStamp(dateAdded / 1000000),
|
StartTime: timestamp(dateAdded / 1000000),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Firefox stores download metadata as: "target_path,{json}"
|
// Firefox stores download metadata as: "target_path,{json}"
|
||||||
@@ -37,7 +36,7 @@ func extractDownloads(path string) ([]types.DownloadEntry, error) {
|
|||||||
entry.TargetPath = contentList[0]
|
entry.TargetPath = contentList[0]
|
||||||
json := "{" + contentList[1]
|
json := "{" + contentList[1]
|
||||||
entry.TotalBytes = gjson.Get(json, "fileSize").Int()
|
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 {
|
} else {
|
||||||
entry.TargetPath = content
|
entry.TargetPath = content
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const firefoxHistoryQuery = `SELECT url, COALESCE(last_visit_date, 0),
|
const firefoxHistoryQuery = `SELECT url, COALESCE(last_visit_date, 0),
|
||||||
@@ -25,7 +24,7 @@ func extractHistories(path string) ([]types.HistoryEntry, error) {
|
|||||||
URL: url,
|
URL: url,
|
||||||
Title: title,
|
Title: title,
|
||||||
VisitCount: visitCount,
|
VisitCount: visitCount,
|
||||||
LastVisit: typeutil.TimeStamp(lastVisit / 1000000),
|
LastVisit: timestamp(lastVisit / 1000000),
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||||
"github.com/moond4rk/hackbrowserdata/log"
|
"github.com/moond4rk/hackbrowserdata/log"
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// decryptPBE combines base64 decode + ASN1 PBE parse + decrypt into one call.
|
// 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,
|
URL: url,
|
||||||
Username: string(user),
|
Username: string(user),
|
||||||
Password: string(pwd),
|
Password: string(pwd),
|
||||||
CreatedAt: typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000),
|
CreatedAt: timestamp(v.Get("timeCreated").Int() / 1000),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const firefoxLocalStorageQuery = `SELECT originKey, key, value FROM webappsstore2`
|
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.
|
// parseOriginKey converts Firefox's reversed origin format to a URL.
|
||||||
// Example: "moc.buhtig.:https:443" → "https://github.com:443"
|
// Example: "moc.buhtig.:https:443" → "https://github.com:443"
|
||||||
func parseOriginKey(originKey string) string {
|
func parseOriginKey(originKey string) string {
|
||||||
@@ -34,7 +41,7 @@ func parseOriginKey(originKey string) string {
|
|||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return originKey
|
return originKey
|
||||||
}
|
}
|
||||||
host := string(typeutil.Reverse([]byte(parts[0])))
|
host := reverseString(parts[0])
|
||||||
host = strings.TrimPrefix(host, ".")
|
host = strings.TrimPrefix(host, ".")
|
||||||
scheme := parts[1]
|
scheme := parts[1]
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/filemanager"
|
"github.com/moond4rk/hackbrowserdata/filemanager"
|
||||||
"github.com/moond4rk/hackbrowserdata/log"
|
"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.
|
// is validated by attempting to decrypt an actual login entry.
|
||||||
func (b *Browser) getMasterKey(session *filemanager.Session, tempPaths map[types.Category]string) ([]byte, error) {
|
func (b *Browser) getMasterKey(session *filemanager.Session, tempPaths map[types.Category]string) ([]byte, error) {
|
||||||
key4Src := filepath.Join(b.profileDir, "key4.db")
|
key4Src := filepath.Join(b.profileDir, "key4.db")
|
||||||
if !fileutil.IsFileExists(key4Src) {
|
if !fileutil.FileExists(key4Src) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
key4Dst := filepath.Join(session.TempDir(), "key4.db")
|
key4Dst := filepath.Join(session.TempDir(), "key4.db")
|
||||||
@@ -236,3 +237,12 @@ func resolveSourcePaths(sources map[types.Category][]sourcePath, profileDir stri
|
|||||||
}
|
}
|
||||||
return resolved
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func TestNewBrowsers(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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)
|
browsers, err := NewBrowsers(cfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
+84
-88
@@ -1,24 +1,30 @@
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/des"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ASN1PBE interface {
|
const des3KeySize = 24 // 3DES uses 24-byte (192-bit) keys
|
||||||
Decrypt(globalSalt []byte) ([]byte, error)
|
|
||||||
|
|
||||||
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) {
|
func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
|
||||||
var (
|
var (
|
||||||
nss nssPBE
|
nss privateKeyPBE
|
||||||
meta metaPBE
|
meta passwordCheckPBE
|
||||||
login loginPBE
|
login credentialPBE
|
||||||
)
|
)
|
||||||
if _, err := asn1.Unmarshal(b, &nss); err == nil {
|
if _, err := asn1.Unmarshal(b, &nss); err == nil {
|
||||||
return nss, nil
|
return nss, nil
|
||||||
@@ -29,12 +35,10 @@ func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
|
|||||||
if _, err := asn1.Unmarshal(b, &login); err == nil {
|
if _, err := asn1.Unmarshal(b, &login); err == nil {
|
||||||
return login, nil
|
return login, nil
|
||||||
}
|
}
|
||||||
return nil, ErrDecodeASN1Failed
|
return nil, errDecodeASN1
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed")
|
// privateKeyPBE Struct
|
||||||
|
|
||||||
// nssPBE Struct
|
|
||||||
//
|
//
|
||||||
// SEQUENCE (2 elem)
|
// SEQUENCE (2 elem)
|
||||||
// OBJECT IDENTIFIER
|
// OBJECT IDENTIFIER
|
||||||
@@ -42,52 +46,56 @@ var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed")
|
|||||||
// OCTET STRING (20 byte)
|
// OCTET STRING (20 byte)
|
||||||
// INTEGER 1
|
// INTEGER 1
|
||||||
// OCTET STRING (16 byte)
|
// OCTET STRING (16 byte)
|
||||||
type nssPBE struct {
|
type privateKeyPBE struct {
|
||||||
AlgoAttr struct {
|
AlgoAttr struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
SaltAttr struct {
|
SaltAttr struct {
|
||||||
EntrySalt []byte
|
EntrySalt []byte
|
||||||
Len int
|
KeyLen int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Encrypted []byte
|
Encrypted []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt decrypts the encrypted password with the global salt.
|
func (n privateKeyPBE) Decrypt(globalSalt []byte) ([]byte, error) {
|
||||||
func (n nssPBE) Decrypt(globalSalt []byte) ([]byte, error) {
|
|
||||||
key, iv := n.deriveKeyAndIV(globalSalt)
|
key, iv := n.deriveKeyAndIV(globalSalt)
|
||||||
|
|
||||||
return DES3Decrypt(key, iv, n.Encrypted)
|
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)
|
key, iv := n.deriveKeyAndIV(globalSalt)
|
||||||
|
|
||||||
return DES3Encrypt(key, iv, plaintext)
|
return DES3Encrypt(key, iv, plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deriveKeyAndIV derives the key and initialization vector (IV)
|
// deriveKeyAndIV implements NSS PBE-SHA1-3DES key derivation.
|
||||||
// from the global salt and entry salt.
|
// Reference: https://searchfox.org/mozilla-central/source/security/nss/lib/softoken/lowpbe.c
|
||||||
func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
|
//
|
||||||
salt := n.AlgoAttr.SaltAttr.EntrySalt
|
// Derivation steps:
|
||||||
hashPrefix := sha1.Sum(globalSalt)
|
//
|
||||||
compositeHash := sha1.Sum(append(hashPrefix[:], salt...))
|
// hp = SHA1(globalSalt)
|
||||||
paddedEntrySalt := paddingZero(salt, 20)
|
// 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[:])
|
hmac1 := hmac.New(sha1.New, ck[:])
|
||||||
hmacProcessor.Write(paddedEntrySalt)
|
hmac1.Write(paddedSalt)
|
||||||
|
|
||||||
paddedEntrySalt = append(paddedEntrySalt, salt...)
|
k1 := hmac.New(sha1.New, ck[:])
|
||||||
keyComponent1 := hmac.New(sha1.New, compositeHash[:])
|
k1.Write(append(paddedSalt, entrySalt...))
|
||||||
keyComponent1.Write(paddedEntrySalt)
|
|
||||||
|
|
||||||
hmacWithSalt := append(hmacProcessor.Sum(nil), salt...)
|
k2 := hmac.New(sha1.New, ck[:])
|
||||||
keyComponent2 := hmac.New(sha1.New, compositeHash[:])
|
k2.Write(append(hmac1.Sum(nil), entrySalt...))
|
||||||
keyComponent2.Write(hmacWithSalt)
|
|
||||||
|
|
||||||
key := append(keyComponent1.Sum(nil), keyComponent2.Sum(nil)...)
|
dk := append(k1.Sum(nil), k2.Sum(nil)...)
|
||||||
iv := key[len(key)-8:]
|
return dk[:24], dk[len(dk)-8:]
|
||||||
return key[:24], iv
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetaPBE Struct
|
// MetaPBE Struct
|
||||||
@@ -107,17 +115,17 @@ func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
|
|||||||
// OBJECT IDENTIFIER
|
// OBJECT IDENTIFIER
|
||||||
// OCTET STRING (14 byte)
|
// OCTET STRING (14 byte)
|
||||||
// OCTET STRING (16 byte)
|
// OCTET STRING (16 byte)
|
||||||
type metaPBE struct {
|
type passwordCheckPBE struct {
|
||||||
AlgoAttr algoAttr
|
AlgoAttr algoAttr
|
||||||
Encrypted []byte
|
Encrypted []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type algoAttr struct {
|
type algoAttr struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
Data struct {
|
KDFParams struct {
|
||||||
Data struct {
|
PBKDF2 struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
SlatAttr slatAttr
|
SaltAttr saltAttr
|
||||||
}
|
}
|
||||||
IVData ivAttr
|
IVData ivAttr
|
||||||
}
|
}
|
||||||
@@ -128,7 +136,7 @@ type ivAttr struct {
|
|||||||
IV []byte
|
IV []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type slatAttr struct {
|
type saltAttr struct {
|
||||||
EntrySalt []byte
|
EntrySalt []byte
|
||||||
IterationCount int
|
IterationCount int
|
||||||
KeySize 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)
|
key, iv := m.deriveKeyAndIV(globalSalt)
|
||||||
|
return AESCBCDecrypt(key, iv, m.Encrypted)
|
||||||
return AES128CBCDecrypt(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)
|
key, iv := m.deriveKeyAndIV(globalSalt)
|
||||||
|
return AESCBCEncrypt(key, iv, plaintext)
|
||||||
return AES128CBCEncrypt(key, iv, plaintext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m metaPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
|
func (m passwordCheckPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
|
||||||
password := sha1.Sum(globalSalt)
|
password := sha1.Sum(globalSalt)
|
||||||
|
|
||||||
salt := m.AlgoAttr.Data.Data.SlatAttr.EntrySalt
|
params := m.AlgoAttr.KDFParams.PBKDF2.SaltAttr
|
||||||
iter := m.AlgoAttr.Data.Data.SlatAttr.IterationCount
|
key := PBKDF2Key(password[:], params.EntrySalt, params.IterationCount, params.KeySize, sha256.New)
|
||||||
keyLen := m.AlgoAttr.Data.Data.SlatAttr.KeySize
|
|
||||||
|
|
||||||
key := PBKDF2Key(password[:], salt, iter, keyLen, sha256.New)
|
// Firefox stores the IV with its ASN.1 OCTET STRING header (tag=0x04, length=0x0E).
|
||||||
iv := append([]byte{4, 14}, m.AlgoAttr.Data.IVData.IV...)
|
// 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
|
return key, iv
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginPBE Struct
|
// credentialPBE Struct
|
||||||
//
|
//
|
||||||
// OCTET STRING (16 byte)
|
// OCTET STRING (16 byte)
|
||||||
// SEQUENCE (2 elem)
|
// SEQUENCE (2 elem)
|
||||||
// OBJECT IDENTIFIER
|
// OBJECT IDENTIFIER
|
||||||
// OCTET STRING (8 byte)
|
// OCTET STRING (8 byte)
|
||||||
// OCTET STRING (16 byte)
|
// OCTET STRING (16 byte)
|
||||||
type loginPBE struct {
|
type credentialPBE struct {
|
||||||
CipherText []byte
|
KeyCheck []byte
|
||||||
Data struct {
|
Algo struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
IV []byte
|
IV []byte
|
||||||
}
|
}
|
||||||
Encrypted []byte
|
Encrypted []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l loginPBE) Decrypt(globalSalt []byte) ([]byte, error) {
|
func (l credentialPBE) Decrypt(masterKey []byte) ([]byte, error) {
|
||||||
key, iv := l.deriveKeyAndIV(globalSalt)
|
key, iv := l.deriveKeyAndIV(masterKey)
|
||||||
// The encryption algorithm can be reliably inferred from IV length:
|
// The cipher is inferred from IV length (avoids fragile OID checks):
|
||||||
// - 8 bytes : 3DES-CBC (legacy Firefox versions)
|
switch len(iv) {
|
||||||
// - 16 bytes : AES-CBC (Firefox 144+)
|
case des.BlockSize: // 8: 3DES-CBC (legacy Firefox)
|
||||||
if len(iv) == 8 {
|
return DES3Decrypt(key[:des3KeySize], iv, l.Encrypted)
|
||||||
// Use 3DES for old Firefox versions
|
case aes.BlockSize: // 16: AES-256-CBC (Firefox 144+)
|
||||||
return DES3Decrypt(key[:24], iv, l.Encrypted)
|
return AESCBCDecrypt(key, iv, l.Encrypted)
|
||||||
} else if len(iv) == 16 {
|
default:
|
||||||
// Firefox 144+ uses 32-byte keys (AES-256-CBC)
|
return nil, errUnsupportedIVLen
|
||||||
// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths
|
|
||||||
return AES128CBCDecrypt(key, iv, l.Encrypted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("unsupported IV length for loginPBE decryption")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l loginPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
|
func (l credentialPBE) Encrypt(masterKey, plaintext []byte) ([]byte, error) {
|
||||||
key, iv := l.deriveKeyAndIV(globalSalt)
|
key, iv := l.deriveKeyAndIV(masterKey)
|
||||||
// The encryption algorithm can be reliably inferred from IV length:
|
switch len(iv) {
|
||||||
// - 8 bytes : 3DES-CBC (legacy Firefox versions)
|
case des.BlockSize:
|
||||||
// - 16 bytes : AES-CBC (Firefox 144+)
|
return DES3Encrypt(key[:des3KeySize], iv, plaintext)
|
||||||
// This avoids relying on NSS-specific OIDs, which have changed historically.
|
case aes.BlockSize:
|
||||||
if len(iv) == 8 {
|
return AESCBCEncrypt(key, iv, plaintext)
|
||||||
// Use 3DES for old Firefox versions
|
default:
|
||||||
return DES3Encrypt(key[:24], iv, plaintext)
|
return nil, errUnsupportedIVLen
|
||||||
} 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("unsupported IV length for loginPBE encryption")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l loginPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
|
func (l credentialPBE) deriveKeyAndIV(masterKey []byte) ([]byte, []byte) {
|
||||||
return globalSalt, l.Data.IV
|
return masterKey, l.Algo.IV
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-78
@@ -11,18 +11,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pbeIV = []byte("01234567") // 8 bytes
|
pbeIV = []byte("01234567") // 8 bytes
|
||||||
pbePlaintext = []byte("Hello, World!")
|
pbePlaintext = []byte("Hello, World!")
|
||||||
pbeCipherText = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}
|
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}
|
objWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3}
|
||||||
objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46}
|
objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46}
|
||||||
objWithSHA1AndAES = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}
|
objWithSHA1AndAES = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}
|
||||||
nssPBETestCases = []struct {
|
privateKeyPBETestCases = []struct {
|
||||||
RawHexPBE string
|
RawHexPBE string
|
||||||
GlobalSalt []byte
|
GlobalSalt []byte
|
||||||
Encrypted []byte
|
Encrypted []byte
|
||||||
IterationCount int
|
IterationCount int
|
||||||
Len int
|
KeyLen int
|
||||||
Plaintext []byte
|
Plaintext []byte
|
||||||
ObjectIdentifier asn1.ObjectIdentifier
|
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},
|
Encrypted: []byte{0x95, 0x18, 0x3a, 0x14, 0xc7, 0x52, 0xe7, 0xb1, 0xd0, 0xaa, 0xa4, 0x7f, 0x53, 0xe0, 0x50, 0x97},
|
||||||
Plaintext: pbePlaintext,
|
Plaintext: pbePlaintext,
|
||||||
IterationCount: 1,
|
IterationCount: 1,
|
||||||
Len: 32,
|
KeyLen: 32,
|
||||||
ObjectIdentifier: objWithSHA1AndAES,
|
ObjectIdentifier: objWithSHA1AndAES,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
metaPBETestCases = []struct {
|
passwordCheckPBETestCases = []struct {
|
||||||
RawHexPBE string
|
RawHexPBE string
|
||||||
GlobalSalt []byte
|
GlobalSalt []byte
|
||||||
Encrypted []byte
|
Encrypted []byte
|
||||||
@@ -53,7 +53,7 @@ var (
|
|||||||
ObjectIdentifier: objWithSHA256AndAES,
|
ObjectIdentifier: objWithSHA256AndAES,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
loginPBETestCases = []struct {
|
credentialPBETestCases = []struct {
|
||||||
RawHexPBE string
|
RawHexPBE string
|
||||||
GlobalSalt []byte
|
GlobalSalt []byte
|
||||||
Encrypted []byte
|
Encrypted []byte
|
||||||
@@ -73,108 +73,108 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewASN1PBE(t *testing.T) {
|
func TestNewASN1PBE(t *testing.T) {
|
||||||
for _, tc := range nssPBETestCases {
|
for _, tc := range privateKeyPBETestCases {
|
||||||
nssRaw, err := hex.DecodeString(tc.RawHexPBE)
|
nssRaw, err := hex.DecodeString(tc.RawHexPBE)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pbe, err := NewASN1PBE(nssRaw)
|
pbe, err := NewASN1PBE(nssRaw)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
nssPBETC, ok := pbe.(nssPBE)
|
privateKeyPBETC, ok := pbe.(privateKeyPBE)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, nssPBETC.Encrypted, tc.Encrypted)
|
assert.Equal(t, privateKeyPBETC.Encrypted, tc.Encrypted)
|
||||||
assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt)
|
assert.Equal(t, privateKeyPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt)
|
||||||
assert.Equal(t, 20, nssPBETC.AlgoAttr.SaltAttr.Len)
|
assert.Equal(t, 20, privateKeyPBETC.AlgoAttr.SaltAttr.KeyLen)
|
||||||
assert.Equal(t, nssPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier)
|
assert.Equal(t, privateKeyPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNssPBE_Encrypt(t *testing.T) {
|
func TestPrivateKeyPBE_Encrypt(t *testing.T) {
|
||||||
for _, tc := range nssPBETestCases {
|
for _, tc := range privateKeyPBETestCases {
|
||||||
nssPBETC := nssPBE{
|
privateKeyPBETC := privateKeyPBE{
|
||||||
Encrypted: tc.Encrypted,
|
Encrypted: tc.Encrypted,
|
||||||
AlgoAttr: struct {
|
AlgoAttr: struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
SaltAttr struct {
|
SaltAttr struct {
|
||||||
EntrySalt []byte
|
EntrySalt []byte
|
||||||
Len int
|
KeyLen int
|
||||||
}
|
}
|
||||||
}{
|
}{
|
||||||
ObjectIdentifier: tc.ObjectIdentifier,
|
ObjectIdentifier: tc.ObjectIdentifier,
|
||||||
SaltAttr: struct {
|
SaltAttr: struct {
|
||||||
EntrySalt []byte
|
EntrySalt []byte
|
||||||
Len int
|
KeyLen int
|
||||||
}{
|
}{
|
||||||
EntrySalt: tc.GlobalSalt,
|
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)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, encrypted)
|
assert.NotEmpty(t, encrypted)
|
||||||
assert.Equal(t, nssPBETC.Encrypted, encrypted)
|
assert.Equal(t, privateKeyPBETC.Encrypted, encrypted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNssPBE_Decrypt(t *testing.T) {
|
func TestPrivateKeyPBE_Decrypt(t *testing.T) {
|
||||||
for _, tc := range nssPBETestCases {
|
for _, tc := range privateKeyPBETestCases {
|
||||||
nssPBETC := nssPBE{
|
privateKeyPBETC := privateKeyPBE{
|
||||||
Encrypted: tc.Encrypted,
|
Encrypted: tc.Encrypted,
|
||||||
AlgoAttr: struct {
|
AlgoAttr: struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
SaltAttr struct {
|
SaltAttr struct {
|
||||||
EntrySalt []byte
|
EntrySalt []byte
|
||||||
Len int
|
KeyLen int
|
||||||
}
|
}
|
||||||
}{
|
}{
|
||||||
ObjectIdentifier: tc.ObjectIdentifier,
|
ObjectIdentifier: tc.ObjectIdentifier,
|
||||||
SaltAttr: struct {
|
SaltAttr: struct {
|
||||||
EntrySalt []byte
|
EntrySalt []byte
|
||||||
Len int
|
KeyLen int
|
||||||
}{
|
}{
|
||||||
EntrySalt: tc.GlobalSalt,
|
EntrySalt: tc.GlobalSalt,
|
||||||
Len: 20,
|
KeyLen: 20,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
decrypted, err := nssPBETC.Decrypt(tc.GlobalSalt)
|
decrypted, err := privateKeyPBETC.Decrypt(tc.GlobalSalt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, decrypted)
|
assert.NotEmpty(t, decrypted)
|
||||||
assert.Equal(t, pbePlaintext, decrypted)
|
assert.Equal(t, pbePlaintext, decrypted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewASN1PBE_MetaPBE(t *testing.T) {
|
func TestNewASN1PBE_PasswordCheckPBE(t *testing.T) {
|
||||||
for _, tc := range metaPBETestCases {
|
for _, tc := range passwordCheckPBETestCases {
|
||||||
metaRaw, err := hex.DecodeString(tc.RawHexPBE)
|
metaRaw, err := hex.DecodeString(tc.RawHexPBE)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pbe, err := NewASN1PBE(metaRaw)
|
pbe, err := NewASN1PBE(metaRaw)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metaPBETC, ok := pbe.(metaPBE)
|
passwordCheckPBETC, ok := pbe.(passwordCheckPBE)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, metaPBETC.Encrypted, tc.Encrypted)
|
assert.Equal(t, passwordCheckPBETC.Encrypted, tc.Encrypted)
|
||||||
assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.IV, tc.IV)
|
assert.Equal(t, passwordCheckPBETC.AlgoAttr.KDFParams.IVData.IV, tc.IV)
|
||||||
assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.ObjectIdentifier, objWithSHA256AndAES)
|
assert.Equal(t, passwordCheckPBETC.AlgoAttr.KDFParams.IVData.ObjectIdentifier, objWithSHA256AndAES)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetaPBE_Encrypt(t *testing.T) {
|
func TestPasswordCheckPBE_Encrypt(t *testing.T) {
|
||||||
for _, tc := range metaPBETestCases {
|
for _, tc := range passwordCheckPBETestCases {
|
||||||
metaPBETC := metaPBE{
|
passwordCheckPBETC := passwordCheckPBE{
|
||||||
AlgoAttr: algoAttr{
|
AlgoAttr: algoAttr{
|
||||||
ObjectIdentifier: tc.ObjectIdentifier,
|
ObjectIdentifier: tc.ObjectIdentifier,
|
||||||
Data: struct {
|
KDFParams: struct {
|
||||||
Data struct {
|
PBKDF2 struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
SlatAttr slatAttr
|
SaltAttr saltAttr
|
||||||
}
|
}
|
||||||
IVData ivAttr
|
IVData ivAttr
|
||||||
}{
|
}{
|
||||||
Data: struct {
|
PBKDF2: struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
SlatAttr slatAttr
|
SaltAttr saltAttr
|
||||||
}{
|
}{
|
||||||
ObjectIdentifier: tc.ObjectIdentifier,
|
ObjectIdentifier: tc.ObjectIdentifier,
|
||||||
SlatAttr: slatAttr{
|
SaltAttr: saltAttr{
|
||||||
EntrySalt: tc.GlobalSalt,
|
EntrySalt: tc.GlobalSalt,
|
||||||
IterationCount: 1,
|
IterationCount: 1,
|
||||||
KeySize: 32,
|
KeySize: 32,
|
||||||
@@ -193,31 +193,31 @@ func TestMetaPBE_Encrypt(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Encrypted: tc.Encrypted,
|
Encrypted: tc.Encrypted,
|
||||||
}
|
}
|
||||||
encrypted, err := metaPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
|
encrypted, err := passwordCheckPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, encrypted)
|
assert.NotEmpty(t, encrypted)
|
||||||
assert.Equal(t, metaPBETC.Encrypted, encrypted)
|
assert.Equal(t, passwordCheckPBETC.Encrypted, encrypted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetaPBE_Decrypt(t *testing.T) {
|
func TestPasswordCheckPBE_Decrypt(t *testing.T) {
|
||||||
for _, tc := range metaPBETestCases {
|
for _, tc := range passwordCheckPBETestCases {
|
||||||
metaPBETC := metaPBE{
|
passwordCheckPBETC := passwordCheckPBE{
|
||||||
AlgoAttr: algoAttr{
|
AlgoAttr: algoAttr{
|
||||||
ObjectIdentifier: tc.ObjectIdentifier,
|
ObjectIdentifier: tc.ObjectIdentifier,
|
||||||
Data: struct {
|
KDFParams: struct {
|
||||||
Data struct {
|
PBKDF2 struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
SlatAttr slatAttr
|
SaltAttr saltAttr
|
||||||
}
|
}
|
||||||
IVData ivAttr
|
IVData ivAttr
|
||||||
}{
|
}{
|
||||||
Data: struct {
|
PBKDF2: struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
SlatAttr slatAttr
|
SaltAttr saltAttr
|
||||||
}{
|
}{
|
||||||
ObjectIdentifier: tc.ObjectIdentifier,
|
ObjectIdentifier: tc.ObjectIdentifier,
|
||||||
SlatAttr: slatAttr{
|
SaltAttr: saltAttr{
|
||||||
EntrySalt: tc.GlobalSalt,
|
EntrySalt: tc.GlobalSalt,
|
||||||
IterationCount: 1,
|
IterationCount: 1,
|
||||||
KeySize: 32,
|
KeySize: 32,
|
||||||
@@ -236,32 +236,32 @@ func TestMetaPBE_Decrypt(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Encrypted: tc.Encrypted,
|
Encrypted: tc.Encrypted,
|
||||||
}
|
}
|
||||||
decrypted, err := metaPBETC.Decrypt(tc.GlobalSalt)
|
decrypted, err := passwordCheckPBETC.Decrypt(tc.GlobalSalt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, decrypted)
|
assert.NotEmpty(t, decrypted)
|
||||||
assert.Equal(t, pbePlaintext, decrypted)
|
assert.Equal(t, pbePlaintext, decrypted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewASN1PBE_LoginPBE(t *testing.T) {
|
func TestNewASN1PBE_CredentialPBE(t *testing.T) {
|
||||||
for _, tc := range loginPBETestCases {
|
for _, tc := range credentialPBETestCases {
|
||||||
loginRaw, err := hex.DecodeString(tc.RawHexPBE)
|
loginRaw, err := hex.DecodeString(tc.RawHexPBE)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pbe, err := NewASN1PBE(loginRaw)
|
pbe, err := NewASN1PBE(loginRaw)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
loginPBETC, ok := pbe.(loginPBE)
|
credentialPBETC, ok := pbe.(credentialPBE)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, loginPBETC.Encrypted, tc.Encrypted)
|
assert.Equal(t, credentialPBETC.Encrypted, tc.Encrypted)
|
||||||
assert.Equal(t, loginPBETC.Data.IV, tc.IV)
|
assert.Equal(t, credentialPBETC.Algo.IV, tc.IV)
|
||||||
assert.Equal(t, loginPBETC.Data.ObjectIdentifier, objWithMD5AndDESCBC)
|
assert.Equal(t, credentialPBETC.Algo.ObjectIdentifier, objWithMD5AndDESCBC)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginPBE_Encrypt(t *testing.T) {
|
func TestCredentialPBE_Encrypt(t *testing.T) {
|
||||||
for _, tc := range loginPBETestCases {
|
for _, tc := range credentialPBETestCases {
|
||||||
loginPBETC := loginPBE{
|
credentialPBETC := credentialPBE{
|
||||||
CipherText: pbeCipherText,
|
KeyCheck: pbeKeyCheck,
|
||||||
Data: struct {
|
Algo: struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
IV []byte
|
IV []byte
|
||||||
}{
|
}{
|
||||||
@@ -270,18 +270,18 @@ func TestLoginPBE_Encrypt(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Encrypted: tc.Encrypted,
|
Encrypted: tc.Encrypted,
|
||||||
}
|
}
|
||||||
encrypted, err := loginPBETC.Encrypt(tc.GlobalSalt, plainText)
|
encrypted, err := credentialPBETC.Encrypt(tc.GlobalSalt, plainText)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, encrypted)
|
assert.NotEmpty(t, encrypted)
|
||||||
assert.Equal(t, loginPBETC.Encrypted, encrypted)
|
assert.Equal(t, credentialPBETC.Encrypted, encrypted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginPBE_Decrypt(t *testing.T) {
|
func TestCredentialPBE_Decrypt(t *testing.T) {
|
||||||
for _, tc := range loginPBETestCases {
|
for _, tc := range credentialPBETestCases {
|
||||||
loginPBETC := loginPBE{
|
credentialPBETC := credentialPBE{
|
||||||
CipherText: pbeCipherText,
|
KeyCheck: pbeKeyCheck,
|
||||||
Data: struct {
|
Algo: struct {
|
||||||
asn1.ObjectIdentifier
|
asn1.ObjectIdentifier
|
||||||
IV []byte
|
IV []byte
|
||||||
}{
|
}{
|
||||||
@@ -290,9 +290,63 @@ func TestLoginPBE_Decrypt(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Encrypted: tc.Encrypted,
|
Encrypted: tc.Encrypted,
|
||||||
}
|
}
|
||||||
decrypted, err := loginPBETC.Decrypt(tc.GlobalSalt)
|
decrypted, err := credentialPBETC.Decrypt(tc.GlobalSalt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, decrypted)
|
assert.NotEmpty(t, decrypted)
|
||||||
assert.Equal(t, pbePlaintext, 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)
|
||||||
|
}
|
||||||
|
|||||||
+95
-102
@@ -1,161 +1,154 @@
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/des"
|
"crypto/des"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrCiphertextLengthIsInvalid = errors.New("ciphertext length is invalid")
|
// 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).
|
||||||
// AES128CBCDecrypt decrypts data using AES-CBC mode.
|
func AESCBCEncrypt(key, iv, plaintext []byte) ([]byte, error) {
|
||||||
// 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) {
|
|
||||||
block, err := aes.NewCipher(key)
|
block, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Check ciphertext length
|
return cbcEncrypt(block, iv, plaintext)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
block, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return cbcDecrypt(block, iv, ciphertext)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DES3Encrypt encrypts data using 3DES-CBC mode with PKCS5 padding.
|
||||||
func DES3Encrypt(key, iv, plaintext []byte) ([]byte, error) {
|
func DES3Encrypt(key, iv, plaintext []byte) ([]byte, error) {
|
||||||
block, err := des.NewTripleDESCipher(key)
|
block, err := des.NewTripleDESCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return cbcEncrypt(block, iv, plaintext)
|
||||||
plaintext = pkcs5Padding(plaintext, block.BlockSize())
|
|
||||||
dst := make([]byte, len(plaintext))
|
|
||||||
blockMode := cipher.NewCBCEncrypter(block, iv)
|
|
||||||
blockMode.CryptBlocks(dst, plaintext)
|
|
||||||
|
|
||||||
return dst, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AESGCMDecrypt chromium > 80 https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/sync/os_crypt_win.cc
|
// DES3Decrypt decrypts data using 3DES-CBC mode with PKCS5 unpadding.
|
||||||
func AESGCMDecrypt(key, nounce, ciphertext []byte) ([]byte, error) {
|
func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
|
||||||
block, err := aes.NewCipher(key)
|
block, err := des.NewTripleDESCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
blockMode, err := cipher.NewGCM(block)
|
return cbcDecrypt(block, iv, ciphertext)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
origData, err := blockMode.Open(nil, nounce, ciphertext, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return origData, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AESGCMEncrypt encrypts plaintext using AES encryption in GCM mode.
|
// AESGCMEncrypt encrypts data using AES-GCM mode.
|
||||||
func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) {
|
func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) {
|
||||||
block, err := aes.NewCipher(key)
|
block, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
blockMode, err := cipher.NewGCM(block)
|
aead, err := cipher.NewGCM(block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// The first parameter is the prefix for the output, we can leave it nil.
|
if len(nonce) != aead.NonceSize() {
|
||||||
// The Seal method encrypts and authenticates the data, appending the result to the dst.
|
return nil, errInvalidNonceLen
|
||||||
encryptedData := blockMode.Seal(nil, nonce, plaintext, nil)
|
}
|
||||||
return encryptedData, nil
|
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 {
|
func paddingZero(src []byte, length int) []byte {
|
||||||
padding := length - len(src)
|
if len(src) >= length {
|
||||||
if padding <= 0 {
|
|
||||||
return src
|
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)
|
length := len(src)
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return nil, errors.New("pkcs5UnPadding: src should not be empty")
|
return nil, errInvalidPadding
|
||||||
}
|
}
|
||||||
padding := int(src[length-1])
|
padding := int(src[length-1])
|
||||||
if padding < 1 || padding > aes.BlockSize {
|
if padding < 1 || padding > blockSize || padding > length {
|
||||||
return nil, errors.New("pkcs5UnPadding: invalid padding size")
|
return nil, errInvalidPadding
|
||||||
}
|
|
||||||
if padding > length {
|
|
||||||
return nil, errors.New("pkcs5UnPadding: invalid padding length")
|
|
||||||
}
|
}
|
||||||
for _, b := range src[length-padding:] {
|
for _, b := range src[length-padding:] {
|
||||||
if int(b) != padding {
|
if int(b) != padding {
|
||||||
return nil, errors.New("pkcs5UnPadding: invalid padding content")
|
return nil, errInvalidPadding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return src[:length-padding], nil
|
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...)
|
|
||||||
}
|
|
||||||
|
|||||||
+13
-9
@@ -2,18 +2,22 @@
|
|||||||
|
|
||||||
package crypto
|
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) {
|
const minCBCDataSize = versionPrefixLen + aes.BlockSize // "v10" + one AES block = 19 bytes minimum
|
||||||
if len(password) <= 3 {
|
|
||||||
return nil, ErrCiphertextLengthIsInvalid
|
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 AESCBCDecrypt(key, chromiumCBCIV, ciphertext[versionPrefixLen:])
|
||||||
return AES128CBCDecrypt(key, iv, password[3:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecryptWithDPAPI(_ []byte) ([]byte, error) {
|
func DecryptDPAPI(_ []byte) ([]byte, error) {
|
||||||
return nil, ErrDarwinNotSupportDPAPI
|
return nil, errDPAPINotSupported
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-7
@@ -2,14 +2,22 @@
|
|||||||
|
|
||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
func DecryptWithChromium(key, encryptPass []byte) ([]byte, error) {
|
import (
|
||||||
if len(encryptPass) < 3 {
|
"bytes"
|
||||||
return nil, ErrCiphertextLengthIsInvalid
|
"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 AESCBCDecrypt(key, chromiumCBCIV, ciphertext[versionPrefixLen:])
|
||||||
return AES128CBCDecrypt(key, iv, encryptPass[3:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecryptWithDPAPI(_ []byte) ([]byte, error) {
|
func DecryptDPAPI(_ []byte) ([]byte, error) {
|
||||||
return nil, nil
|
return nil, errDPAPINotSupported
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-4
@@ -27,16 +27,16 @@ var (
|
|||||||
aesGCMCiphertext = "6c49dac89992639713edab3a114c450968a08b53556872cea3919e2e9a"
|
aesGCMCiphertext = "6c49dac89992639713edab3a114c450968a08b53556872cea3919e2e9a"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAES128CBCEncrypt(t *testing.T) {
|
func TestAESCBCEncrypt(t *testing.T) {
|
||||||
encrypted, err := AES128CBCEncrypt(aesKey, aesIV, plainText)
|
encrypted, err := AESCBCEncrypt(aesKey, aesIV, plainText)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, encrypted)
|
assert.NotEmpty(t, encrypted)
|
||||||
assert.Equal(t, aes128Ciphertext, fmt.Sprintf("%x", encrypted))
|
assert.Equal(t, aes128Ciphertext, fmt.Sprintf("%x", encrypted))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAES128CBCDecrypt(t *testing.T) {
|
func TestAESCBCDecrypt(t *testing.T) {
|
||||||
ciphertext, _ := hex.DecodeString(aes128Ciphertext)
|
ciphertext, _ := hex.DecodeString(aes128Ciphertext)
|
||||||
decrypted, err := AES128CBCDecrypt(aesKey, aesIV, ciphertext)
|
decrypted, err := AESCBCDecrypt(aesKey, aesIV, ciphertext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, decrypted)
|
assert.NotEmpty(t, decrypted)
|
||||||
assert.Equal(t, plainText, decrypted)
|
assert.Equal(t, plainText, decrypted)
|
||||||
@@ -71,3 +71,117 @@ func TestAESGCMDecrypt(t *testing.T) {
|
|||||||
assert.NotEmpty(t, decrypted)
|
assert.NotEmpty(t, decrypted)
|
||||||
assert.Equal(t, plainText, 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)
|
||||||
|
}
|
||||||
|
|||||||
+19
-25
@@ -9,35 +9,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Assuming the nonce size is 12 bytes and the minimum encrypted data size is 3 bytes
|
gcmNonceSize = 12 // AES-GCM standard nonce size
|
||||||
minEncryptedDataSize = 15
|
minGCMDataSize = versionPrefixLen + gcmNonceSize // "v10" + nonce = 15 bytes minimum
|
||||||
nonceSize = 12
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DecryptWithChromium(key, ciphertext []byte) ([]byte, error) {
|
func DecryptChromium(key, ciphertext []byte) ([]byte, error) {
|
||||||
if len(ciphertext) < minEncryptedDataSize {
|
if len(ciphertext) < minGCMDataSize {
|
||||||
return nil, ErrCiphertextLengthIsInvalid
|
return nil, errShortCiphertext
|
||||||
}
|
}
|
||||||
|
nonce := ciphertext[versionPrefixLen : versionPrefixLen+gcmNonceSize]
|
||||||
nonce := ciphertext[3 : 3+nonceSize]
|
payload := ciphertext[versionPrefixLen+gcmNonceSize:]
|
||||||
encryptedPassword := ciphertext[3+nonceSize:]
|
return AESGCMDecrypt(key, nonce, payload)
|
||||||
|
|
||||||
return AESGCMDecrypt(key, nonce, encryptedPassword)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptWithYandex decrypts the password with AES-GCM
|
// DecryptYandex decrypts a Yandex-encrypted value.
|
||||||
func DecryptWithYandex(key, ciphertext []byte) ([]byte, error) {
|
// TODO: Yandex uses the same AES-GCM format as Chromium for now;
|
||||||
if len(ciphertext) < minEncryptedDataSize {
|
// update when Yandex-specific decryption diverges.
|
||||||
return nil, ErrCiphertextLengthIsInvalid
|
func DecryptYandex(key, ciphertext []byte) ([]byte, error) {
|
||||||
|
if len(ciphertext) < minGCMDataSize {
|
||||||
|
return nil, errShortCiphertext
|
||||||
}
|
}
|
||||||
// remove Prefix 'v10'
|
nonce := ciphertext[versionPrefixLen : versionPrefixLen+gcmNonceSize]
|
||||||
// gcmBlockSize = 16
|
payload := ciphertext[versionPrefixLen+gcmNonceSize:]
|
||||||
// gcmTagSize = 16
|
return AESGCMDecrypt(key, nonce, payload)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type dataBlob struct {
|
type dataBlob struct {
|
||||||
@@ -61,11 +55,11 @@ func (b *dataBlob) bytes() []byte {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptWithDPAPI (Data Protection Application Programming Interface)
|
// DecryptDPAPI (Data Protection Application Programming Interface)
|
||||||
// is a simple cryptographic application programming interface
|
// is a simple cryptographic application programming interface
|
||||||
// available as a built-in component in Windows 2000 and
|
// available as a built-in component in Windows 2000 and
|
||||||
// later versions of Microsoft Windows operating systems
|
// later versions of Microsoft Windows operating systems
|
||||||
func DecryptWithDPAPI(ciphertext []byte) ([]byte, error) {
|
func DecryptDPAPI(ciphertext []byte) ([]byte, error) {
|
||||||
crypt32 := syscall.NewLazyDLL("Crypt32.dll")
|
crypt32 := syscall.NewLazyDLL("Crypt32.dll")
|
||||||
kernel32 := syscall.NewLazyDLL("Kernel32.dll")
|
kernel32 := syscall.NewLazyDLL("Kernel32.dll")
|
||||||
unprotectDataProc := crypt32.NewProc("CryptUnprotectData")
|
unprotectDataProc := crypt32.NewProc("CryptUnprotectData")
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -69,7 +69,7 @@ type addressRange struct {
|
|||||||
// DecryptKeychain extracts the browser storage password from login.keychain-db
|
// DecryptKeychain extracts the browser storage password from login.keychain-db
|
||||||
// by dumping securityd memory and scanning for the keychain master key.
|
// by dumping securityd memory and scanning for the keychain master key.
|
||||||
// Requires root privileges.
|
// Requires root privileges.
|
||||||
func DecryptKeychain(storagename string) (string, error) {
|
func DecryptKeychain(storageName string) (string, error) {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
return "", errors.New("requires root privileges")
|
return "", errors.New("requires root privileges")
|
||||||
}
|
}
|
||||||
@@ -124,13 +124,13 @@ func DecryptKeychain(storagename string) (string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, rec := range records {
|
for _, rec := range records {
|
||||||
if rec.Account == storagename {
|
if rec.Account == storageName {
|
||||||
return string(rec.Password), nil
|
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.
|
// scanMasterKeyCandidates scans the core dump for 24-byte master key candidates.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
var darwinParams = pbkdf2Params{
|
var darwinParams = pbkdf2Params{
|
||||||
salt: []byte("saltysalt"),
|
salt: []byte("saltysalt"),
|
||||||
iterations: 1003,
|
iterations: 1003,
|
||||||
keyLen: 16,
|
keySize: 16,
|
||||||
hashFunc: sha1.New,
|
hashFunc: sha1.New,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
var linuxParams = pbkdf2Params{
|
var linuxParams = pbkdf2Params{
|
||||||
salt: []byte("saltysalt"),
|
salt: []byte("saltysalt"),
|
||||||
iterations: 1,
|
iterations: 1,
|
||||||
keyLen: 16,
|
keySize: 16,
|
||||||
hashFunc: sha1.New,
|
hashFunc: sha1.New,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("DPAPI decrypt: %w", err)
|
return nil, fmt.Errorf("DPAPI decrypt: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
type pbkdf2Params struct {
|
type pbkdf2Params struct {
|
||||||
salt []byte
|
salt []byte
|
||||||
iterations int
|
iterations int
|
||||||
keyLen int
|
keySize int
|
||||||
hashFunc func() hash.Hash
|
hashFunc func() hash.Hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// deriveKey derives an encryption key from a secret using PBKDF2.
|
// deriveKey derives an encryption key from a secret using PBKDF2.
|
||||||
func (p pbkdf2Params) deriveKey(secret []byte) []byte {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ import (
|
|||||||
// Using a higher iteration count will increase the cost of an exhaustive
|
// Using a higher iteration count will increase the cost of an exhaustive
|
||||||
// search but will also make derivation proportionally slower.
|
// search but will also make derivation proportionally slower.
|
||||||
// Copy from https://golang.org/x/crypto/pbkdf2
|
// 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)
|
prf := hmac.New(h, password)
|
||||||
hashLen := prf.Size()
|
hashLen := prf.Size()
|
||||||
numBlocks := (keyLen + hashLen - 1) / hashLen
|
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:]
|
t := dk[len(dk)-hashLen:]
|
||||||
copy(u, t)
|
copy(u, t)
|
||||||
|
|
||||||
for n := 2; n <= iter; n++ {
|
for n := 2; n <= iterations; n++ {
|
||||||
prf.Reset()
|
prf.Reset()
|
||||||
prf.Write(u)
|
prf.Write(u)
|
||||||
u = u[:0]
|
u = u[:0]
|
||||||
|
|||||||
@@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-5
@@ -12,14 +12,17 @@ const (
|
|||||||
|
|
||||||
// CipherDPAPI is pre-Chrome 80 raw DPAPI encryption (no version prefix).
|
// CipherDPAPI is pre-Chrome 80 raw DPAPI encryption (no version prefix).
|
||||||
CipherDPAPI CipherVersion = "dpapi"
|
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.
|
// DetectVersion identifies the encryption version from a ciphertext prefix.
|
||||||
func DetectVersion(ciphertext []byte) CipherVersion {
|
func DetectVersion(ciphertext []byte) CipherVersion {
|
||||||
if len(ciphertext) < 3 {
|
if len(ciphertext) < versionPrefixLen {
|
||||||
return CipherDPAPI
|
return CipherDPAPI
|
||||||
}
|
}
|
||||||
prefix := string(ciphertext[:3])
|
prefix := string(ciphertext[:versionPrefixLen])
|
||||||
switch prefix {
|
switch prefix {
|
||||||
case "v10":
|
case "v10":
|
||||||
return CipherV10
|
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.
|
// Returns the ciphertext unchanged if no known prefix is found.
|
||||||
func StripPrefix(ciphertext []byte) []byte {
|
func stripPrefix(ciphertext []byte) []byte {
|
||||||
ver := DetectVersion(ciphertext)
|
ver := DetectVersion(ciphertext)
|
||||||
if ver == CipherV10 || ver == CipherV20 {
|
if ver == CipherV10 || ver == CipherV20 {
|
||||||
return ciphertext[3:]
|
return ciphertext[versionPrefixLen:]
|
||||||
}
|
}
|
||||||
return ciphertext
|
return ciphertext
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestDetectVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStripPrefix(t *testing.T) {
|
func Test_stripPrefix(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
ciphertext []byte
|
ciphertext []byte
|
||||||
@@ -41,7 +41,7 @@ func TestStripPrefix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
assert.Equal(t, tt.want, StripPrefix(tt.ciphertext))
|
assert.Equal(t, tt.want, stripPrefix(tt.ciphertext))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const (
|
|||||||
|
|
||||||
statusInfoLengthMismatch = 0xC0000004
|
statusInfoLengthMismatch = 0xC0000004
|
||||||
|
|
||||||
fileMAPRead = 0x0004
|
fileMapRead = 0x0004
|
||||||
pageReadonly = 0x02
|
pageReadonly = 0x02
|
||||||
fileTypeDisk = 0x0001
|
fileTypeDisk = 0x0001
|
||||||
)
|
)
|
||||||
@@ -288,7 +288,7 @@ func readViaFileMapping(handle windows.Handle, size int) ([]byte, error) {
|
|||||||
defer windows.CloseHandle(windows.Handle(mapping))
|
defer windows.CloseHandle(windows.Handle(mapping))
|
||||||
|
|
||||||
viewPtr, _, err := procMapViewOfFile.Call(
|
viewPtr, _, err := procMapViewOfFile.Call(
|
||||||
mapping, fileMAPRead,
|
mapping, fileMapRead,
|
||||||
0, 0, 0,
|
0, 0, 0,
|
||||||
)
|
)
|
||||||
if viewPtr == 0 {
|
if viewPtr == 0 {
|
||||||
|
|||||||
+4
-4
@@ -74,10 +74,10 @@ func NonSensitiveCategories() []Category {
|
|||||||
type BrowserKind int
|
type BrowserKind int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KindChromium BrowserKind = iota
|
Chromium BrowserKind = iota
|
||||||
KindChromiumYandex // Chromium variant with different file names and extract logic
|
ChromiumYandex // Chromium variant with different file names and extract logic
|
||||||
KindChromiumOpera // Opera: extensions in "opsettings" key, data in Roaming
|
ChromiumOpera // Opera: extensions in "opsettings" key, data in Roaming
|
||||||
KindFirefox
|
Firefox
|
||||||
)
|
)
|
||||||
|
|
||||||
// BrowserConfig holds the declarative configuration for a browser installation.
|
// BrowserConfig holds the declarative configuration for a browser installation.
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsFileExists checks if the file exists in the provided path
|
// FileExists checks if the file exists in the provided path.
|
||||||
func IsFileExists(filename string) bool {
|
func FileExists(filename string) bool {
|
||||||
info, err := os.Stat(filename)
|
info, err := os.Stat(filename)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return false
|
return false
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user