416 lines
10 KiB
Go
416 lines
10 KiB
Go
package wechat
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"crypto/sha512"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"io"
|
|
"os"
|
|
|
|
"golang.org/x/crypto/pbkdf2"
|
|
)
|
|
|
|
// Constants for WeChat database decryption
|
|
const (
|
|
// Common constants
|
|
PageSize = 4096
|
|
KeySize = 32
|
|
SaltSize = 16
|
|
AESBlockSize = 16
|
|
SQLiteHeader = "SQLite format 3\x00"
|
|
|
|
// Version specific constants
|
|
V3IterCount = 64000
|
|
V4IterCount = 256000
|
|
|
|
IVSize = 16 // Same for both versions
|
|
HmacSHA1Size = 20 // Used in V3
|
|
HmacSHA512Size = 64 // Used in V4
|
|
)
|
|
|
|
// Error definitions
|
|
var (
|
|
ErrHashVerificationFailed = errors.New("hash verification failed")
|
|
ErrInvalidVersion = errors.New("invalid version, must be 3 or 4")
|
|
ErrInvalidKey = errors.New("invalid key format")
|
|
ErrIncorrectKey = errors.New("incorrect decryption key")
|
|
ErrReadFile = errors.New("failed to read database file")
|
|
ErrOpenFile = errors.New("failed to open database file")
|
|
ErrIncompleteRead = errors.New("incomplete header read")
|
|
ErrCreateCipher = errors.New("failed to create cipher")
|
|
ErrDecodeKey = errors.New("failed to decode hex key")
|
|
ErrWriteOutput = errors.New("failed to write output")
|
|
ErrSeekFile = errors.New("failed to seek in file")
|
|
ErrOperationCanceled = errors.New("operation was canceled")
|
|
ErrAlreadyDecrypted = errors.New("file is already decrypted")
|
|
)
|
|
|
|
// Decryptor handles the decryption of WeChat database files
|
|
type Decryptor struct {
|
|
// Database file path
|
|
dbPath string
|
|
|
|
// Database properties
|
|
version int
|
|
salt []byte
|
|
page1 []byte
|
|
reserve int
|
|
|
|
// Calculated fields
|
|
hashFunc func() hash.Hash
|
|
hmacSize int
|
|
currentPage int64
|
|
totalPages int64
|
|
}
|
|
|
|
// NewDecryptor creates a new Decryptor for the specified database file and version
|
|
func NewDecryptor(dbPath string, version int) (*Decryptor, error) {
|
|
// Validate version
|
|
if version != 3 && version != 4 {
|
|
return nil, ErrInvalidVersion
|
|
}
|
|
|
|
// Open database file
|
|
fp, err := os.Open(dbPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrOpenFile, err)
|
|
}
|
|
defer fp.Close()
|
|
|
|
// Get file size
|
|
fileInfo, err := fp.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get file info: %v", err)
|
|
}
|
|
|
|
// Calculate total pages
|
|
fileSize := fileInfo.Size()
|
|
totalPages := fileSize / PageSize
|
|
if fileSize%PageSize > 0 {
|
|
totalPages++
|
|
}
|
|
|
|
// Read first page
|
|
buffer := make([]byte, PageSize)
|
|
n, err := io.ReadFull(fp, buffer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrReadFile, err)
|
|
}
|
|
if n != PageSize {
|
|
return nil, fmt.Errorf("%w: expected %d bytes, got %d", ErrIncompleteRead, PageSize, n)
|
|
}
|
|
|
|
// Check if file is already decrypted
|
|
if bytes.Equal(buffer[:len(SQLiteHeader)-1], []byte(SQLiteHeader[:len(SQLiteHeader)-1])) {
|
|
return nil, ErrAlreadyDecrypted
|
|
}
|
|
|
|
// Initialize hash function and HMAC size based on version
|
|
var hashFunc func() hash.Hash
|
|
var hmacSize int
|
|
|
|
if version == 4 {
|
|
hashFunc = sha512.New
|
|
hmacSize = HmacSHA512Size
|
|
} else {
|
|
hashFunc = sha1.New
|
|
hmacSize = HmacSHA1Size
|
|
}
|
|
|
|
// Calculate reserve size and MAC offset
|
|
reserve := IVSize + hmacSize
|
|
if reserve%AESBlockSize != 0 {
|
|
reserve = ((reserve / AESBlockSize) + 1) * AESBlockSize
|
|
}
|
|
|
|
return &Decryptor{
|
|
dbPath: dbPath,
|
|
version: version,
|
|
salt: buffer[:SaltSize],
|
|
page1: buffer,
|
|
reserve: reserve,
|
|
hashFunc: hashFunc,
|
|
hmacSize: hmacSize,
|
|
totalPages: totalPages,
|
|
}, nil
|
|
}
|
|
|
|
// GetTotalPages returns the total number of pages in the database
|
|
func (d *Decryptor) GetTotalPages() int64 {
|
|
return d.totalPages
|
|
}
|
|
|
|
// Validate checks if the provided key is valid for this database
|
|
func (d *Decryptor) Validate(key []byte) bool {
|
|
if len(key) != KeySize {
|
|
return false
|
|
}
|
|
_, macKey := d.calcPBKDF2Key(key)
|
|
return d.validate(macKey)
|
|
}
|
|
|
|
func (d *Decryptor) calcPBKDF2Key(key []byte) ([]byte, []byte) {
|
|
// Generate encryption key from password
|
|
var encKey []byte
|
|
if d.version == 4 {
|
|
encKey = pbkdf2.Key(key, d.salt, V4IterCount, KeySize, sha512.New)
|
|
} else {
|
|
encKey = pbkdf2.Key(key, d.salt, V3IterCount, KeySize, sha1.New)
|
|
}
|
|
|
|
// Generate MAC key
|
|
macSalt := xorBytes(d.salt, 0x3a)
|
|
macKey := pbkdf2.Key(encKey, macSalt, 2, KeySize, d.hashFunc)
|
|
return encKey, macKey
|
|
}
|
|
|
|
func (d *Decryptor) validate(macKey []byte) bool {
|
|
// Calculate HMAC
|
|
hashMac := hmac.New(d.hashFunc, macKey)
|
|
|
|
dataEnd := PageSize - d.reserve + IVSize
|
|
hashMac.Write(d.page1[SaltSize:dataEnd])
|
|
|
|
// Page number is fixed as 1
|
|
pageNoBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(pageNoBytes, 1)
|
|
hashMac.Write(pageNoBytes)
|
|
|
|
calculatedMAC := hashMac.Sum(nil)
|
|
storedMAC := d.page1[dataEnd : dataEnd+d.hmacSize]
|
|
|
|
return hmac.Equal(calculatedMAC, storedMAC)
|
|
}
|
|
|
|
// Decrypt decrypts the database using the provided key and writes the result to the writer
|
|
func (d *Decryptor) Decrypt(ctx context.Context, hexKey string, w io.Writer) error {
|
|
// Decode key
|
|
key, err := hex.DecodeString(hexKey)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrDecodeKey, err)
|
|
}
|
|
|
|
encKey, macKey := d.calcPBKDF2Key(key)
|
|
|
|
// Validate key first
|
|
if !d.validate(macKey) {
|
|
return ErrIncorrectKey
|
|
}
|
|
|
|
// Open input file
|
|
dbFile, err := os.Open(d.dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrOpenFile, err)
|
|
}
|
|
defer dbFile.Close()
|
|
|
|
// Write SQLite header to output
|
|
_, err = w.Write([]byte(SQLiteHeader))
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrWriteOutput, err)
|
|
}
|
|
|
|
// Process each page
|
|
pageBuf := make([]byte, PageSize)
|
|
d.currentPage = 0
|
|
|
|
for curPage := int64(0); curPage < d.totalPages; curPage++ {
|
|
// Check for cancellation before processing each page
|
|
select {
|
|
case <-ctx.Done():
|
|
return ErrOperationCanceled
|
|
default:
|
|
// Continue processing
|
|
}
|
|
|
|
// For the first page, we need to skip the salt
|
|
if curPage == 0 {
|
|
// Read the first page
|
|
_, err = io.ReadFull(dbFile, pageBuf)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrReadFile, err)
|
|
}
|
|
} else {
|
|
// Read a full page
|
|
n, err := io.ReadFull(dbFile, pageBuf)
|
|
if err != nil {
|
|
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
// Handle last partial page
|
|
if n > 0 {
|
|
// Process partial page
|
|
// For simplicity, we'll just break here
|
|
break
|
|
}
|
|
}
|
|
return fmt.Errorf("%w: %v", ErrReadFile, err)
|
|
}
|
|
}
|
|
|
|
// check if page contains only zeros (v3 & v4 both have this behavior)
|
|
allZeros := true
|
|
for _, b := range pageBuf {
|
|
if b != 0 {
|
|
allZeros = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if allZeros {
|
|
// Write the zeros page to output
|
|
_, err = w.Write(pageBuf)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrWriteOutput, err)
|
|
}
|
|
|
|
// Update progress
|
|
d.currentPage = curPage + 1
|
|
continue
|
|
|
|
// // Set current page to total pages to indicate completion
|
|
// d.currentPage = d.totalPages
|
|
// return nil
|
|
}
|
|
|
|
// Decrypt the page
|
|
decryptedPage, err := d.decryptPage(encKey, macKey, pageBuf, curPage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write decrypted page to output
|
|
_, err = w.Write(decryptedPage)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrWriteOutput, err)
|
|
}
|
|
|
|
// Update progress
|
|
d.currentPage = curPage + 1
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// decryptPage decrypts a single page of the database
|
|
func (d *Decryptor) decryptPage(key, macKey []byte, pageBuf []byte, pageNum int64) ([]byte, error) {
|
|
offset := 0
|
|
if pageNum == 0 {
|
|
offset = SaltSize
|
|
}
|
|
|
|
// Verify HMAC
|
|
mac := hmac.New(d.hashFunc, macKey)
|
|
mac.Write(pageBuf[offset : PageSize-d.reserve+IVSize])
|
|
|
|
// Convert page number and update HMAC
|
|
pageNumBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(pageNumBytes, uint32(pageNum+1))
|
|
mac.Write(pageNumBytes)
|
|
|
|
hashMac := mac.Sum(nil)
|
|
|
|
hashMacStartOffset := PageSize - d.reserve + IVSize
|
|
hashMacEndOffset := hashMacStartOffset + len(hashMac)
|
|
|
|
if !bytes.Equal(hashMac, pageBuf[hashMacStartOffset:hashMacEndOffset]) {
|
|
return nil, ErrHashVerificationFailed
|
|
}
|
|
|
|
// Decrypt content using AES-256-CBC
|
|
iv := pageBuf[PageSize-d.reserve : PageSize-d.reserve+IVSize]
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrCreateCipher, err)
|
|
}
|
|
|
|
mode := cipher.NewCBCDecrypter(block, iv)
|
|
|
|
// Create a copy of encrypted data for decryption
|
|
encrypted := make([]byte, PageSize-d.reserve-offset)
|
|
copy(encrypted, pageBuf[offset:PageSize-d.reserve])
|
|
|
|
// Decrypt in place
|
|
mode.CryptBlocks(encrypted, encrypted)
|
|
|
|
// Combine decrypted data with reserve part
|
|
decryptedPage := append(encrypted, pageBuf[PageSize-d.reserve:PageSize]...)
|
|
|
|
return decryptedPage, nil
|
|
}
|
|
|
|
// xorBytes performs XOR operation on each byte of the array with the specified byte
|
|
func xorBytes(a []byte, b byte) []byte {
|
|
result := make([]byte, len(a))
|
|
for i := range a {
|
|
result[i] = a[i] ^ b
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Utility functions for backward compatibility
|
|
|
|
// DecryptDBFile decrypts a WeChat database file and returns the decrypted content
|
|
func DecryptDBFile(dbPath string, hexKey string, version int) ([]byte, error) {
|
|
// Create a buffer to store the decrypted content
|
|
var buf bytes.Buffer
|
|
|
|
// Create a decryptor
|
|
d, err := NewDecryptor(dbPath, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Decrypt the database
|
|
err = d.Decrypt(context.Background(), hexKey, &buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// DecryptDBFileToFile decrypts a WeChat database file and saves the result to the specified output file
|
|
func DecryptDBFileToFile(dbPath, outputPath, hexKey string, version int) error {
|
|
// Create output file
|
|
outputFile, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %v", err)
|
|
}
|
|
defer outputFile.Close()
|
|
|
|
// Create a decryptor
|
|
d, err := NewDecryptor(dbPath, version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Decrypt the database
|
|
return d.Decrypt(context.Background(), hexKey, outputFile)
|
|
}
|
|
|
|
// ValidateDBKey validates if the provided key is correct for the database
|
|
func ValidateDBKey(dbPath string, hexKey string, version int) bool {
|
|
// Create a decryptor
|
|
d, err := NewDecryptor(dbPath, version)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Decode key
|
|
key, err := hex.DecodeString(hexKey)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Validate the key
|
|
return d.Validate(key)
|
|
}
|