diff --git a/pkg/e2ee/manager.go b/pkg/e2ee/manager.go index 28f8e4e..40b8dc5 100644 --- a/pkg/e2ee/manager.go +++ b/pkg/e2ee/manager.go @@ -329,6 +329,24 @@ func (m *Manager) DecryptMessageV2(msg *line.Message) (string, error) { if err != nil { return "", err } + + ver := 2 + if vStr, ok := msg.ContentMetadata["e2eeVersion"]; ok && vStr == "1" { + ver = 1 + } + + if ver == 1 { + cipher, err := assembleCipherV1(msg.Chunks) + if err != nil { + return "", err + } + pt, _, err := m.runner.ChannelDecryptV1(chanID, senderKeyID, receiverKeyID, base64.StdEncoding.EncodeToString(cipher)) + if err != nil { + return "", err + } + return pt, nil + } + pt, _, err := m.runner.ChannelDecryptV2(chanID, msg.To, msg.From, senderKeyID, receiverKeyID, msg.ContentType, base64.StdEncoding.EncodeToString(cipher)) if err != nil { return "", err @@ -336,6 +354,26 @@ func (m *Manager) DecryptMessageV2(msg *line.Message) (string, error) { return pt, nil } +func assembleCipherV1(chunks []string) ([]byte, error) { + if len(chunks) < 3 { + return nil, fmt.Errorf("not enough chunks") + } + decode := func(s string) ([]byte, error) { return base64.StdEncoding.DecodeString(strings.TrimSpace(s)) } + b0, err := decode(chunks[0]) + if err != nil { + return nil, err + } + b1, err := decode(chunks[1]) + if err != nil { + return nil, err + } + b2, err := decode(chunks[2]) + if err != nil { + return nil, err + } + return append(append(b0, b1...), b2...), nil +} + func (m *Manager) UnwrapGroupSharedKey(chatMid string, sharedKey *line.E2EEGroupSharedKey) (int, error) { m.mu.Lock() defer m.mu.Unlock() diff --git a/pkg/internal/runner.js b/pkg/internal/runner.js index 195b6bc..6ef90e5 100644 --- a/pkg/internal/runner.js +++ b/pkg/internal/runner.js @@ -398,6 +398,21 @@ async function run() { process.stdout.write( JSON.stringify({ ciphertext: u8ToB64(ct) }) + "\n" ); + } else if (type === "channel_decrypt_v1") { // messages sent from ios LINE app use v1 + const { + channelId, + ciphertext, + } = query; + const chan = getChannel(toIntStrict("channelId", channelId)); + + const u8Cipher = b64ToU8(ciphertext); + const pt = chan.decryptV1( + u8Cipher + ); + const plaintext = new TextDecoder().decode(Uint8Array.from(pt).buffer); + process.stdout.write( + JSON.stringify({ plaintext, base64: u8ToB64(pt) }) + "\n" + ); } else if (type === "channel_decrypt_v2") { const { channelId, diff --git a/pkg/runner.go b/pkg/runner.go index 0e4a6bc..0883711 100644 --- a/pkg/runner.go +++ b/pkg/runner.go @@ -404,6 +404,28 @@ func (r *Runner) ChannelEncryptV2(channelID int, to, from string, senderKeyID, r return ct, nil } +// decrypts ciphertext with channel v1 (ios) +func (r *Runner) ChannelDecryptV1(channelID, senderKeyID, receiverKeyID int, ciphertext string) (string, string, error) { + resp, err := r.call(map[string]any{ + "type": "channel_decrypt_v1", + "channelId": channelID, + "senderKeyId": senderKeyID, + "receiverKeyId": receiverKeyID, + "ciphertext": ciphertext, + }) + if err != nil { + return "", "", err + } + var plaintext, base64 string + if v, ok := resp["plaintext"]; ok { + _ = json.Unmarshal(v, &plaintext) + } + if v, ok := resp["base64"]; ok { + _ = json.Unmarshal(v, &base64) + } + return plaintext, base64, nil +} + // decrypts ciphertext with channel v2 func (r *Runner) ChannelDecryptV2(channelID int, to, from string, senderKeyID, receiverKeyID, contentType int, ciphertext string) (string, string, error) { resp, err := r.call(map[string]any{