diff --git a/demo.py b/demo.py index 12ee930..77dbab1 100644 --- a/demo.py +++ b/demo.py @@ -15,7 +15,6 @@ from rich.logging import RichHandler import apns import ids -from ids.keydec import IdentityKeys logging.basicConfig( level=logging.NOTSET, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()] @@ -66,13 +65,11 @@ else: user.authenticate(username, password) -user.ec_key = CONFIG.get("encryption", {}).get("ec_key") -user.rsa_key = CONFIG.get("encryption", {}).get("rsa_key") +user.encryption_identity = ids.identity.IDSIdentity(encryption_key=CONFIG.get("encryption", {}).get("rsa_key"), signing_key=CONFIG.get("encryption", {}).get("ec_key")) if ( CONFIG.get("id", {}).get("cert") is not None - and user.ec_key is not None - and user.rsa_key is not None + and user.encryption_identity is not None ): id_keypair = ids._helpers.KeyPair(CONFIG["id"]["key"], CONFIG["id"]["cert"]) user.restore_identity(id_keypair) @@ -100,8 +97,8 @@ threading.Thread(target=keepalive, daemon=True).start() # Write config.json CONFIG["encryption"] = { - "ec_key": user.ec_key, - "rsa_key": user.rsa_key, + "rsa_key": user.encryption_identity.encryption_key, + "ec_key": user.encryption_identity.signing_key, } CONFIG["id"] = { "key": user._id_keypair.key, @@ -122,8 +119,7 @@ CONFIG["push"] = { with open("config.json", "w") as f: json.dump(CONFIG, f, indent=4) -user_rsa_key = load_pem_private_key(user.rsa_key.encode(), password=None) - +user_rsa_key = ids._helpers.parse_key(user.encryption_identity.encryption_key) NORMAL_NONCE = b"\x00" * 15 + b"\x01" def decrypt(payload, sender_token, rsa_key: rsa.RSAPrivateKey = user_rsa_key): @@ -187,9 +183,9 @@ def decrypt(payload, sender_token, rsa_key: rsa.RSAPrivateKey = user_rsa_key): logging.error(f"Failed to find identity for {sender_token}") identity_keys = sender['client-data']['public-message-identity-key'] - identity_keys = IdentityKeys.decode(identity_keys) + identity_keys = ids.identity.IDSIdentity.decode(identity_keys) - sender_ec_key = identity_keys.ecdsa_key + sender_ec_key = ids._helpers.parse_key(identity_keys.signing_public_key) from cryptography.hazmat.primitives.asymmetric import ec #logging.debug(f"Verifying signature {signature} with key {sender_ec_key.public_numbers()} and data {body}") diff --git a/ids/__init__.py b/ids/__init__.py index cd141a4..5d7226b 100644 --- a/ids/__init__.py +++ b/ids/__init__.py @@ -2,7 +2,7 @@ from base64 import b64encode import apns -from . import _helpers, identity, profile, query, keydec +from . import _helpers, identity, profile, query class IDSUser: @@ -60,10 +60,9 @@ class IDSUser: self.ec_key, self.rsa_key will be set to a randomly gnenerated EC and RSA keypair if they are not already set """ - if self.ec_key is None or self.rsa_key is None: - self.ec_key, self.rsa_key, published_keys = keydec.generate_keys() - else: - published_keys = keydec.load_keys(self.ec_key, self.rsa_key) + if self.encryption_identity is None: + self.encryption_identity = identity.IDSIdentity() + cert = identity.register( b64encode(self.push_connection.token), @@ -71,7 +70,7 @@ class IDSUser: self.user_id, self._auth_keypair, self._push_keypair, - published_keys, + self.encryption_identity, validation_data, ) self._id_keypair = _helpers.KeyPair(self._auth_keypair.key, cert) diff --git a/ids/_helpers.py b/ids/_helpers.py index a7e0a6d..579125b 100644 --- a/ids/_helpers.py +++ b/ids/_helpers.py @@ -14,3 +14,29 @@ def dearmour(armoured: str) -> str: return re.sub(r"-----BEGIN .*-----|-----END .*-----", "", armoured).replace( "\n", "" ) + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.types import ( + PrivateKeyTypes, + PublicKeyTypes, +) +def parse_key(key: str): + # Check if it is a public or private key + if "PUBLIC" in key: + return serialization.load_pem_public_key(key.encode()) + else: + return serialization.load_pem_private_key(key.encode(), None) + +def serialize_key(key: PrivateKeyTypes | PublicKeyTypes): + if isinstance(key, PrivateKeyTypes): + return key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ).decode("utf-8").strip() + else: + return key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode("utf-8").strip() + \ No newline at end of file diff --git a/ids/identity.py b/ids/identity.py index c90f943..51fa0b9 100644 --- a/ids/identity.py +++ b/ids/identity.py @@ -3,16 +3,93 @@ from base64 import b64decode import requests -from ._helpers import PROTOCOL_VERSION, USER_AGENT, KeyPair +from ._helpers import PROTOCOL_VERSION, USER_AGENT, KeyPair, parse_key, serialize_key from .signing import add_auth_signature, armour_cert -from .keydec import IdentityKeys + +from io import BytesIO + +from cryptography.hazmat.primitives.asymmetric import ec, rsa import logging logger = logging.getLogger("ids") +class IDSIdentity: + def __init__(self, signing_key: str | None = None, encryption_key: str | None = None, signing_public_key: str | None = None, encryption_public_key: str | None = None): + if signing_key is not None: + self.signing_key = signing_key + self.signing_public_key = serialize_key(parse_key(signing_key).public_key()) + elif signing_public_key is not None: + self.signing_key = None + self.signing_public_key = signing_public_key + else: + # Generate a new key + self.signing_key = serialize_key(ec.generate_private_key(ec.SECP256R1())) + self.signing_public_key = serialize_key(parse_key(self.signing_key).public_key()) + + if encryption_key is not None: + self.encryption_key = encryption_key + self.encryption_public_key = serialize_key(parse_key(encryption_key).public_key()) + elif encryption_public_key is not None: + self.encryption_key = None + self.encryption_public_key = encryption_public_key + else: + self.encryption_key = serialize_key(rsa.generate_private_key(65537, 1280)) + self.encryption_public_key = serialize_key(parse_key(self.encryption_key).public_key()) + + def decode(input: bytes) -> 'IDSIdentity': + input = BytesIO(input) + assert input.read(5) == b'\x30\x81\xF6\x81\x43' # DER header + raw_ecdsa = input.read(67) + assert input.read(3) == b'\x82\x81\xAE' # DER header + raw_rsa = input.read(174) + + # Parse the RSA key + raw_rsa = BytesIO(raw_rsa) + assert raw_rsa.read(2) == b'\x00\xAC' # Not sure what this is + assert raw_rsa.read(3) == b'\x30\x81\xA9' # Inner DER header + assert raw_rsa.read(3) == b'\x02\x81\xA1' + rsa_modulus = raw_rsa.read(161) + rsa_modulus = int.from_bytes(rsa_modulus, "big") + assert raw_rsa.read(5) == b'\x02\x03\x01\x00\x01' # Exponent, should always be 65537 + + # Parse the EC key + assert raw_ecdsa[:3] == b'\x00\x41\x04' + raw_ecdsa = raw_ecdsa[3:] + ec_x = int.from_bytes(raw_ecdsa[:32], "big") + ec_y = int.from_bytes(raw_ecdsa[32:], "big") + + ec_key = ec.EllipticCurvePublicNumbers(ec_x, ec_y, ec.SECP256R1()) + ec_key = ec_key.public_key() + + rsa_key = rsa.RSAPublicNumbers(e=65537, n=rsa_modulus) + rsa_key = rsa_key.public_key() + + return IDSIdentity(signing_public_key=serialize_key(ec_key), encryption_public_key=serialize_key(rsa_key)) + + + def encode(self) -> bytes: + output = BytesIO() + + raw_rsa = BytesIO() + raw_rsa.write(b'\x00\xAC') + raw_rsa.write(b'\x30\x81\xA9') + raw_rsa.write(b'\x02\x81\xA1') + raw_rsa.write(parse_key(self.encryption_public_key).public_numbers().n.to_bytes(161, "big")) + raw_rsa.write(b'\x02\x03\x01\x00\x01') # Hardcode the exponent + + output.write(b'\x30\x81\xF6\x81\x43') + output.write(b'\x00\x41\x04') + output.write(parse_key(self.signing_public_key).public_numbers().x.to_bytes(32, "big")) + output.write(parse_key(self.signing_public_key).public_numbers().y.to_bytes(32, "big")) + + output.write(b'\x82\x81\xAE') + output.write(raw_rsa.getvalue()) + + return output.getvalue() + def register( - push_token, handles, user_id, auth_key: KeyPair, push_key: KeyPair, published_keys: IdentityKeys, validation_data + push_token, handles, user_id, auth_key: KeyPair, push_key: KeyPair, identity: IDSIdentity, validation_data ): logger.debug(f"Registering IDS identity for {handles}") uris = [{"uri": handle} for handle in handles] @@ -31,19 +108,7 @@ def register( "client-data": { 'is-c2k-equipment': True, 'optionally-receive-typing-indicators': True, - 'public-message-identity-key': published_keys.encode(), - # 'public-message-identity-key': b64decode("""MIH2gUMAQQSYmvE+hYOWVGotZUCd - # M6zoW/2clK8RIzUtE6JAmWSCwj7d - # B213vxEBNAPHefEtlxkVKlQH6bsw - # ja5qYyl3Fh28goGuAKwwgakCgaEA - # 4lw3MrXOFIWWIi3TTUGksXVCIz92 - # R3AG3ghBa1ZBoZ6rIJHeuxhD2vTV - # hicpW7kvZ/+AFgE4vFFef/9TjG6C - # rsBtWUUfPtYHqc7+uaghVW13qfYC - # tdGsW8Apvf6MJqsRmITJjoYZ5kwl - # scp5Xw/1KVQzKMfZrwZeLC/UZ6O1 - # 41u4Xvm+u40e+Ky/wMCOwLGBG0Ag - # ZBH91Xrq+S8izgSLmQIDAQAB""".replace("\n", "").replace(" ", "").replace("\t", "")), + 'public-message-identity-key': identity.encode(), 'public-message-identity-version':2, 'show-peer-errors': True, 'supports-ack-v1': True, diff --git a/ids/keydec.py b/ids/keydec.py deleted file mode 100644 index 2fe9267..0000000 --- a/ids/keydec.py +++ /dev/null @@ -1,108 +0,0 @@ -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives import serialization - -from base64 import b64decode, b64encode - -from io import BytesIO - -def generate_keys() -> tuple[str, str, 'IdentityKeys']: - """ - ECDSA key, RSA key, IdentityKeys - """ - ecdsa_key = ec.generate_private_key(ec.SECP256R1()) - rsa_key = rsa.generate_private_key(65537, 1280) - - # Serialize the keys into PEM - ecdsa_key_p = ecdsa_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ).decode("utf-8").strip() - rsa_key_p = rsa_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ).decode("utf-8").strip() - return ecdsa_key_p, rsa_key_p, IdentityKeys(ecdsa_key.public_key(), rsa_key.public_key()) - -def load_keys(ecdsa_key_p: str, rsa_key_p: str) -> 'IdentityKeys': - ecdsa_key = serialization.load_pem_private_key(ecdsa_key_p.encode(), password=None) - rsa_key = serialization.load_pem_private_key(rsa_key_p.encode(), password=None) - return IdentityKeys(ecdsa_key.public_key(), rsa_key.public_key()) - -class IdentityKeys(): - def __init__(self, ecdsa_key: ec.EllipticCurvePublicKey, rsa_key: rsa.RSAPublicKey): - self.ecdsa_key = ecdsa_key - self.rsa_key = rsa_key - - def decode(input: bytes) -> 'IdentityKeys': - input = BytesIO(input) - - assert input.read(5) == b'\x30\x81\xF6\x81\x43' # DER header - raw_ecdsa = input.read(67) - assert input.read(3) == b'\x82\x81\xAE' # DER header - raw_rsa = input.read(174) - - # Parse the RSA key - raw_rsa = BytesIO(raw_rsa) - assert raw_rsa.read(2) == b'\x00\xAC' # Not sure what this is - assert raw_rsa.read(3) == b'\x30\x81\xA9' # Inner DER header - assert raw_rsa.read(3) == b'\x02\x81\xA1' - rsa_modulus = raw_rsa.read(161) - rsa_modulus = int.from_bytes(rsa_modulus, "big") - assert raw_rsa.read(5) == b'\x02\x03\x01\x00\x01' # Exponent, should always be 65537 - - # Parse the EC key - assert raw_ecdsa[:3] == b'\x00\x41\x04' - raw_ecdsa = raw_ecdsa[3:] - ec_x = int.from_bytes(raw_ecdsa[:32], "big") - ec_y = int.from_bytes(raw_ecdsa[32:], "big") - - ec_key = ec.EllipticCurvePublicNumbers(ec_x, ec_y, ec.SECP256R1()) - ec_key = ec_key.public_key() - - rsa_key = rsa.RSAPublicNumbers(e=65537, n=rsa_modulus) - rsa_key = rsa_key.public_key() - - return IdentityKeys(ec_key, rsa_key) - - def encode(self) -> bytes: - output = BytesIO() - - raw_rsa = BytesIO() - raw_rsa.write(b'\x00\xAC') - raw_rsa.write(b'\x30\x81\xA9') - raw_rsa.write(b'\x02\x81\xA1') - raw_rsa.write(self.rsa_key.public_numbers().n.to_bytes(161, "big")) - raw_rsa.write(b'\x02\x03\x01\x00\x01') # Hardcode the exponent - - output.write(b'\x30\x81\xF6\x81\x43') - output.write(b'\x00\x41\x04') - output.write(self.ecdsa_key.public_numbers().x.to_bytes(32, "big")) - output.write(self.ecdsa_key.public_numbers().y.to_bytes(32, "big")) - - output.write(b'\x82\x81\xAE') - output.write(raw_rsa.getvalue()) - - return output.getvalue() - -if __name__ == "__main__": - input_key = """MIH2gUMAQQSYmvE+hYOWVGotZUCd - M6zoW/2clK8RIzUtE6JAmWSCwj7d - B213vxEBNAPHefEtlxkVKlQH6bsw - ja5qYyl3Fh28goGuAKwwgakCgaEA - 4lw3MrXOFIWWIi3TTUGksXVCIz92 - R3AG3ghBa1ZBoZ6rIJHeuxhD2vTV - hicpW7kvZ/+AFgE4vFFef/9TjG6C - rsBtWUUfPtYHqc7+uaghVW13qfYC - tdGsW8Apvf6MJqsRmITJjoYZ5kwl - scp5Xw/1KVQzKMfZrwZeLC/UZ6O1 - 41u4Xvm+u40e+Ky/wMCOwLGBG0Ag - ZBH91Xrq+S8izgSLmQIDAQAB""".replace("\n", "").replace(" ", "").replace("\t", "") - keys = IdentityKeys.decode(b64decode(input_key)) - print(b64encode(keys.encode()).decode()) - print(len(keys.encode())) - print(len(b64decode(input_key))) - print(keys.encode() == b64decode(input_key)) - print(keys.rsa_key.key_size) \ No newline at end of file diff --git a/imessage.py b/imessage.py new file mode 100644 index 0000000..e82c5d7 --- /dev/null +++ b/imessage.py @@ -0,0 +1,37 @@ + +# LOW LEVEL imessage function, decryption etc +# Don't handle APNS etc, accept it already setup + +## HAVE ANOTHER FILE TO SETUP EVERYTHING AUTOMATICALLY, etc +# JSON parsing of keys, don't pass around strs?? + +import apns +import ids + +class iMessageUser: + def __init__(self, apns: apns.APNSConnection, ids: ids.IDSUser, encryption_key: str, signing_key: str): + self.apns = apns + self.ids = ids + self.encryption_key = encryption_key + self.signing_key = signing_key + + def _get_raw_messages(self) -> list[dict]: + pass + + def _send_raw_message(self, message: dict): + pass + + def _decrypt_message(self, message: dict) -> dict: + pass + + def _encrypt_message(self, message: dict) -> dict: + pass + + def _sign_message(self, message: dict) -> dict: + pass + + def _verify_message(self, message: dict) -> dict: + pass + + def get_messages(self) -> list[dict]: + pass