feat: parse encrypted isphere packet logs

This commit is contained in:
zhaoyilun
2026-07-09 22:53:23 +08:00
parent de3bb922da
commit 4d5b314f83
5 changed files with 420 additions and 23 deletions

View File

@@ -0,0 +1,52 @@
package logcodec
import (
"crypto/cipher"
"crypto/des"
"encoding/base64"
"errors"
"fmt"
)
var (
policyKey = []byte("hyhccdtm")
policyIV = []byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF}
)
func DecryptLine(ciphertextBase64 string) (string, error) {
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64)
if err != nil {
return "", fmt.Errorf("decode base64 log line: %w", err)
}
if len(ciphertext) == 0 || len(ciphertext)%des.BlockSize != 0 {
return "", fmt.Errorf("ciphertext length %d is not a positive DES block multiple", len(ciphertext))
}
block, err := des.NewCipher(policyKey)
if err != nil {
return "", fmt.Errorf("create DES cipher: %w", err)
}
plain := make([]byte, len(ciphertext))
cipher.NewCBCDecrypter(block, policyIV).CryptBlocks(plain, ciphertext)
plain, err = unpadPKCS7(plain, des.BlockSize)
if err != nil {
return "", err
}
return string(plain), nil
}
func unpadPKCS7(data []byte, blockSize int) ([]byte, error) {
if len(data) == 0 || len(data)%blockSize != 0 {
return nil, errors.New("pkcs7 data is empty or not block aligned")
}
pad := int(data[len(data)-1])
if pad == 0 || pad > blockSize || pad > len(data) {
return nil, fmt.Errorf("invalid pkcs7 padding length %d", pad)
}
for i := len(data) - pad; i < len(data); i++ {
if int(data[i]) != pad {
return nil, errors.New("invalid pkcs7 padding bytes")
}
}
return data[:len(data)-pad], nil
}

View File

@@ -0,0 +1,43 @@
package logcodec
import (
"crypto/cipher"
"crypto/des"
"encoding/base64"
"testing"
)
func TestDecryptLineWithKnownPolicyRoundTrip(t *testing.T) {
plaintext := "--------------------------------------------------------------------------------------------------------------------------------------------\r\n2026/7/7 15:30:07\r\n<message id=\"msg-1\" from=\"sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"receiver@imopenfire1-lanzhou\" type=\"chat\"><body>redacted</body></message>"
ciphertext := encryptLineForTest(t, plaintext)
got, err := DecryptLine(ciphertext)
if err != nil {
t.Fatalf("DecryptLine returned error: %v", err)
}
if got != plaintext {
t.Fatalf("DecryptLine() = %q, want %q", got, plaintext)
}
}
func encryptLineForTest(t *testing.T, plaintext string) string {
t.Helper()
block, err := des.NewCipher([]byte("hyhccdtm"))
if err != nil {
t.Fatalf("NewCipher: %v", err)
}
plain := padPKCS7ForTest([]byte(plaintext), des.BlockSize)
ciphertext := make([]byte, len(plain))
iv := []byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF}
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, plain)
return base64.StdEncoding.EncodeToString(ciphertext)
}
func padPKCS7ForTest(data []byte, blockSize int) []byte {
pad := blockSize - len(data)%blockSize
out := append([]byte(nil), data...)
for i := 0; i < pad; i++ {
out = append(out, byte(pad))
}
return out
}

View File

@@ -0,0 +1,73 @@
package packetlog
import (
"encoding/xml"
"errors"
"fmt"
"strings"
)
type Message struct {
ID string
From string
To string
Type string
Body string
Subject string
SendTime string
ReceiptID string
ReceiptType string
Consumed bool
Read bool
}
type xmppMessage struct {
XMLName xml.Name `xml:"message"`
ID string `xml:"id,attr"`
From string `xml:"from,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"`
Body string `xml:"body"`
Subject string `xml:"subject"`
Receipt xmppReceipt `xml:"received"`
ISphere xmppISphere `xml:"isphere"`
Consumed *struct{} `xml:"consumed"`
Readed *struct{} `xml:"readed"`
}
type xmppReceipt struct {
ID string `xml:"id,attr"`
Type string `xml:"type,attr"`
}
type xmppISphere struct {
SendTime string `xml:"sendtime,attr"`
}
func ParseMessageLog(plaintext string) (Message, error) {
start := strings.Index(plaintext, "<message")
if start < 0 {
return Message{}, errors.New("message stanza not found")
}
payload := strings.TrimSpace(plaintext[start:])
var parsed xmppMessage
if err := xml.Unmarshal([]byte(payload), &parsed); err != nil {
return Message{}, fmt.Errorf("parse xmpp message: %w", err)
}
if parsed.ID == "" {
return Message{}, errors.New("message id is empty")
}
return Message{
ID: parsed.ID,
From: parsed.From,
To: parsed.To,
Type: parsed.Type,
Body: strings.TrimSpace(parsed.Body),
Subject: strings.TrimSpace(parsed.Subject),
SendTime: parsed.ISphere.SendTime,
ReceiptID: parsed.Receipt.ID,
ReceiptType: parsed.Receipt.Type,
Consumed: parsed.Consumed != nil,
Read: parsed.Readed != nil,
}, nil
}

View File

@@ -0,0 +1,27 @@
package packetlog
import "testing"
func TestParseMessageLogExtractsXMPPMessage(t *testing.T) {
plaintext := "--------------------------------------------------------------------------------------------------------------------------------------------\r\n2026/7/7 15:30:07\r\n<message id=\"2de080bf-f5cc-45e2-b229-2df6bcd4afe3\" from=\"sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"receiver@imopenfire1-lanzhou\" type=\"chat\">\r\n <body>redacted.docx</body>\r\n <received xmlns=\"urn:xmpp:receipts\" id=\"167c08c3d710489f812c13dc23d4e404\" type=\"1\" stamp=\"\" />\r\n <subject>FILE_TRANSFER_CANCEL</subject>\r\n <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\">\r\n <alert type=\"0\" />\r\n </isphere>\r\n <consumed />\r\n <readed />\r\n</message>"
got, err := ParseMessageLog(plaintext)
if err != nil {
t.Fatalf("ParseMessageLog returned error: %v", err)
}
if got.ID != "2de080bf-f5cc-45e2-b229-2df6bcd4afe3" {
t.Fatalf("ID = %q", got.ID)
}
if got.From != "sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" || got.To != "receiver@imopenfire1-lanzhou" {
t.Fatalf("from/to = %q/%q", got.From, got.To)
}
if got.Body != "redacted.docx" || got.Subject != "FILE_TRANSFER_CANCEL" {
t.Fatalf("body/subject = %q/%q", got.Body, got.Subject)
}
if got.SendTime != "1783423807000" || got.ReceiptID != "167c08c3d710489f812c13dc23d4e404" || got.ReceiptType != "1" {
t.Fatalf("receipt fields = %+v", got)
}
if !got.Consumed || !got.Read {
t.Fatalf("consumed/read = %v/%v", got.Consumed, got.Read)
}
}