2023-05-09 14:36:33 -05:00
|
|
|
from base64 import b64encode
|
2023-08-21 22:10:04 -05:00
|
|
|
from getpass import getpass
|
|
|
|
import logging
|
2023-05-09 16:03:27 -05:00
|
|
|
|
2023-11-19 15:58:39 -06:00
|
|
|
from .. import apns
|
2023-05-09 16:03:27 -05:00
|
|
|
|
2023-07-27 10:04:57 -05:00
|
|
|
from . import _helpers, identity, profile, query
|
2023-08-17 20:14:44 -05:00
|
|
|
from typing import Callable, Any
|
2023-05-09 14:36:33 -05:00
|
|
|
|
|
|
|
class IDSUser:
|
2023-05-09 15:09:28 -05:00
|
|
|
# Sets self.user_id and self._auth_token
|
2023-05-09 14:36:33 -05:00
|
|
|
def _authenticate_for_token(
|
2023-08-17 20:14:44 -05:00
|
|
|
self, username: str, password: str, factor_callback: Callable | None = None
|
2023-05-09 14:36:33 -05:00
|
|
|
):
|
2023-05-09 19:01:22 -05:00
|
|
|
self.user_id, self._auth_token = profile.get_auth_token(
|
2023-05-09 14:36:33 -05:00
|
|
|
username, password, factor_callback
|
|
|
|
)
|
|
|
|
|
2023-05-09 15:09:28 -05:00
|
|
|
# Sets self._auth_keypair using self.user_id and self._auth_token
|
2023-05-09 14:36:33 -05:00
|
|
|
def _authenticate_for_cert(self):
|
2023-05-09 19:01:22 -05:00
|
|
|
self._auth_keypair = profile.get_auth_cert(self.user_id, self._auth_token)
|
2023-05-09 14:36:33 -05:00
|
|
|
|
|
|
|
# Factor callback will be called if a 2FA code is necessary
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
push_connection: apns.APNSConnection,
|
|
|
|
):
|
|
|
|
self.push_connection = push_connection
|
2023-05-09 16:03:27 -05:00
|
|
|
self._push_keypair = _helpers.KeyPair(
|
2023-08-17 20:14:44 -05:00
|
|
|
self.push_connection.credentials.private_key, self.push_connection.credentials.cert
|
2023-05-09 16:03:27 -05:00
|
|
|
)
|
2023-08-21 22:10:04 -05:00
|
|
|
# set the encryption_identity to a default randomized value so that
|
|
|
|
# it's still valid if we can't pull it from the config
|
|
|
|
self.encryption_identity: identity.IDSIdentity = identity.IDSIdentity()
|
2023-05-09 15:09:28 -05:00
|
|
|
|
2023-07-26 17:49:41 -05:00
|
|
|
self.ec_key = self.rsa_key = None
|
|
|
|
|
2023-05-09 15:09:28 -05:00
|
|
|
def __str__(self):
|
2023-08-17 20:14:44 -05:00
|
|
|
return f"IDSUser(user_id={self.user_id}, handles={self.handles}, push_token={b64encode(self.push_connection.credentials.token).decode()})"
|
2023-05-09 16:03:27 -05:00
|
|
|
|
2023-05-09 15:09:28 -05:00
|
|
|
# Authenticates with a username and password, to create a brand new authentication keypair
|
2023-05-09 16:03:27 -05:00
|
|
|
def authenticate(
|
2023-08-17 20:14:44 -05:00
|
|
|
self, username: str, password: str, factor_callback: Callable | None = None
|
2023-05-09 16:03:27 -05:00
|
|
|
):
|
2023-05-09 14:36:33 -05:00
|
|
|
self._authenticate_for_token(username, password, factor_callback)
|
|
|
|
self._authenticate_for_cert()
|
2023-05-09 19:01:22 -05:00
|
|
|
self.handles = profile.get_handles(
|
2023-08-17 20:14:44 -05:00
|
|
|
b64encode(self.push_connection.credentials.token),
|
2023-05-09 14:36:33 -05:00
|
|
|
self.user_id,
|
|
|
|
self._auth_keypair,
|
2023-05-09 15:09:28 -05:00
|
|
|
self._push_keypair,
|
2023-05-09 14:36:33 -05:00
|
|
|
)
|
2023-07-31 15:30:06 -05:00
|
|
|
self.current_handle = self.handles[0]
|
|
|
|
|
2023-05-09 14:36:33 -05:00
|
|
|
|
2023-05-09 15:09:28 -05:00
|
|
|
# Uses an existing authentication keypair
|
2023-08-22 12:13:31 -05:00
|
|
|
def restore_authentication(self, auth_keypair: _helpers.KeyPair, user_id: str, handles: list[str]):
|
2023-05-09 15:09:28 -05:00
|
|
|
self._auth_keypair = auth_keypair
|
|
|
|
self.user_id = user_id
|
2023-08-22 12:13:31 -05:00
|
|
|
self.handles = handles
|
2023-07-31 15:45:45 -05:00
|
|
|
self.current_handle = self.handles[0]
|
2023-05-09 16:03:27 -05:00
|
|
|
|
2023-05-09 15:09:28 -05:00
|
|
|
# This is a separate call so that the user can make sure the first part succeeds before asking for validation data
|
2023-07-26 17:49:41 -05:00
|
|
|
def register(self, validation_data: str):
|
|
|
|
"""
|
|
|
|
self.ec_key, self.rsa_key will be set to a randomly gnenerated EC and RSA keypair
|
|
|
|
if they are not already set
|
|
|
|
"""
|
2023-05-09 16:03:27 -05:00
|
|
|
cert = identity.register(
|
2023-08-17 20:14:44 -05:00
|
|
|
b64encode(self.push_connection.credentials.token),
|
2023-05-09 15:09:28 -05:00
|
|
|
self.handles,
|
|
|
|
self.user_id,
|
|
|
|
self._auth_keypair,
|
|
|
|
self._push_keypair,
|
2023-07-27 10:04:57 -05:00
|
|
|
self.encryption_identity,
|
2023-05-09 16:03:27 -05:00
|
|
|
validation_data,
|
2023-05-09 15:09:28 -05:00
|
|
|
)
|
2023-05-09 16:03:27 -05:00
|
|
|
self._id_keypair = _helpers.KeyPair(self._auth_keypair.key, cert)
|
|
|
|
|
|
|
|
def restore_identity(self, id_keypair: _helpers.KeyPair):
|
|
|
|
self._id_keypair = id_keypair
|
2023-05-09 17:01:32 -05:00
|
|
|
|
2023-08-21 22:10:04 -05:00
|
|
|
def auth_and_set_encryption_from_config(self, config: dict[str, dict[str, Any]]):
|
|
|
|
|
|
|
|
auth = config.get("auth", {})
|
|
|
|
if (
|
|
|
|
((key := auth.get("key")) is not None) and
|
|
|
|
((cert := auth.get("cert")) is not None) and
|
|
|
|
((user_id := auth.get("user_id")) is not None) and
|
|
|
|
((handles := auth.get("handles")) is not None)
|
|
|
|
):
|
|
|
|
auth_keypair = _helpers.KeyPair(key, cert)
|
|
|
|
self.restore_authentication(auth_keypair, user_id, handles)
|
|
|
|
else:
|
|
|
|
username = input("Username: ")
|
|
|
|
password = getpass("Password: ")
|
|
|
|
|
|
|
|
self.authenticate(username, password)
|
|
|
|
|
|
|
|
encryption: dict[str, str] = config.get("encryption", {})
|
|
|
|
id: dict[str, str] = config.get("id", {})
|
|
|
|
|
|
|
|
if (
|
|
|
|
(rsa_key := encryption.get("rsa_key")) and
|
|
|
|
(signing_key := encryption.get("ec_key")) and
|
|
|
|
(cert := id.get("cert")) and
|
|
|
|
(key := id.get("key"))
|
|
|
|
):
|
|
|
|
self.encryption_identity = identity.IDSIdentity(
|
|
|
|
encryption_key=rsa_key,
|
|
|
|
signing_key=signing_key,
|
|
|
|
)
|
|
|
|
|
|
|
|
id_keypair = _helpers.KeyPair(key, cert)
|
|
|
|
self.restore_identity(id_keypair)
|
|
|
|
else:
|
|
|
|
logging.info("Registering new identity...")
|
|
|
|
import emulated.nac
|
|
|
|
|
|
|
|
vd = emulated.nac.generate_validation_data()
|
|
|
|
vd = b64encode(vd).decode()
|
|
|
|
|
|
|
|
self.register(vd)
|
|
|
|
|
2023-08-17 20:14:44 -05:00
|
|
|
async def lookup(self, uris: list[str], topic: str = "com.apple.madrid") -> Any:
|
|
|
|
return await query.lookup(self.push_connection, self.current_handle, self._id_keypair, uris, topic)
|
2023-08-22 12:13:31 -05:00
|
|
|
|