jonesmarvin8 24f6f1f8ca fixes
2026-04-26 21:29:54 -04:00

144 lines
4.5 KiB
Python

# keycard/secure_channel.py
from hashlib import sha512
from .apdu import APDUResponse
from .crypto.aes import aes_cbc_encrypt, aes_cbc_decrypt
from dataclasses import dataclass
@dataclass
class SecureChannel:
"""
SecureChannel manages a secure communication channel using AES encryption
and MAC authentication.
Attributes:
enc_key (bytes): The AES encryption key for the session.
mac_key (bytes): The AES MAC key for message authentication.
iv (bytes): The initialization vector for AES operations.
authenticated (bool): Indicates if the session is authenticated.
"""
enc_key: bytes
mac_key: bytes
iv: bytes
authenticated: bool = False
@classmethod
def open(
cls,
shared_secret: bytes,
pairing_key: bytes,
salt: bytes,
seed_iv: bytes
) -> "SecureChannel":
"""
Opens a new SecureChannel using the provided cryptographic parameters.
Args:
shared_secret (bytes): The shared secret used for key derivation.
pairing_key (bytes): The pairing key used for key derivation.
salt (bytes): The salt value used in the key derivation process.
seed_iv (bytes): The initialization vector (IV) to seed the
session.
Returns:
SecureChannel: An instance of SecureChannel initialized with
derived encryption and MAC keys, and the provided IV.
"""
digest = sha512(shared_secret + pairing_key + salt).digest()
enc_key, mac_key = digest[:32], digest[32:]
return cls(
enc_key=enc_key,
mac_key=mac_key,
iv=seed_iv,
authenticated=True
)
def wrap_apdu(
self,
cla: int,
ins: int,
p1: int,
p2: int,
data: bytes
) -> bytes:
"""
Wraps an APDU command with secure channel encryption and MAC.
Args:
cla (int): The APDU class byte.
ins (int): The APDU instruction byte.
p1 (int): The APDU parameter 1 byte.
p2 (int): The APDU parameter 2 byte.
data (bytes): The APDU data field to be encrypted.
Returns:
tuple[int, int, int, int, bytes]: The wrapped APDU as a tuple
containing the class, instruction, parameter 1, parameter 2,
and the concatenated MAC and encrypted data.
Raises:
ValueError: If the secure channel is not authenticated and the
instruction is not 0x11.
"""
if not self.authenticated and ins != 0x11:
raise ValueError("Secure channel not authenticated")
encrypted = aes_cbc_encrypt(self.enc_key, self.iv, data)
lc = 16 + len(encrypted)
mac_input = bytes([cla, ins, p1, p2, lc]) + bytes(11) + encrypted
enc_data = aes_cbc_encrypt(
self.mac_key, bytes(16), mac_input, padding=False)
self.iv = enc_data[-16:]
return self.iv + encrypted
def unwrap_response(self, response: APDUResponse) -> tuple[bytes, int]:
"""
Unwraps and verifies a secure channel response.
Args:
response (bytes): The encrypted response bytes to unwrap.
Returns:
tuple[bytes, int]: A tuple containing the decrypted plaintext
(excluding the status word) and the status word as an integer.
Raises:
ValueError: If the secure channel is not authenticated.
ValueError: If the response length is invalid.
ValueError: If the MAC verification fails.
ValueError: If the decrypted plaintext is too short to contain a
status word.
"""
if not self.authenticated:
raise ValueError("Secure channel not authenticated")
if len(response.data) < 18:
raise ValueError("Invalid secure response length")
received_mac = bytes(response.data[:16])
encrypted = bytes(response.data[16:])
lr = len(response.data)
mac_input = bytes([lr]) + bytes(15) + bytes(encrypted)
expected_mac = aes_cbc_encrypt(
self.mac_key, bytes(16), mac_input, padding=False)[-16:]
if received_mac != expected_mac:
raise ValueError("Invalid MAC")
plaintext = aes_cbc_decrypt(self.enc_key, self.iv, encrypted)
self.iv = received_mac
if len(plaintext) < 2:
raise ValueError("Missing status word in response")
return plaintext[:-2], int.from_bytes(plaintext[-2:], "big")