initial commit
This commit is contained in:
@@ -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, ×Used, &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
|
||||
}
|
||||
Reference in New Issue
Block a user