76 lines
2.2 KiB
Python
Raw Normal View History

2026-04-26 21:29:54 -04:00
import hashlib
from os import urandom
from typing import Optional
from .. import constants
from ..card_interface import CardInterface
from ..crypto.generate_pairing_token import generate_pairing_token
from ..exceptions import InvalidResponseError
from ..preconditions import require_initialized
@require_initialized
def pair(
card: CardInterface,
shared_secret: str | bytes,
pairing_mode: Optional[constants.PairingMode] = constants.PairingMode.ANY
) -> tuple[int, bytes]:
'''
Performs an ECDH-based pairing handshake with the card.
Args:
card: The keycard interface.
shared_secret: A 32-byte secret or a passphrase convertible to one.
pairing_mode: Mode for pairing: ANY, EPHEMERAL, PERSISTENT
Returns:
tuple[int, bytes]: Pairing index and derived 32-byte pairing key.
Raises:
ValueError: If the shared secret is not 32 bytes.
APDUError: If the card returns a non-success status word.
InvalidResponseError: If response lengths or values are unexpected.
'''
if isinstance(shared_secret, str):
shared_secret = generate_pairing_token(shared_secret)
if len(shared_secret) != 32:
raise ValueError('Shared secret must be 32 bytes')
client_challenge = urandom(32)
response = card.send_apdu(
ins=constants.INS_PAIR,
p2=pairing_mode,
data=client_challenge
)
if len(response) != 64:
raise InvalidResponseError('Unexpected response length')
card_cryptogram = response[:32]
card_challenge = response[32:]
expected = hashlib.sha256(shared_secret + client_challenge).digest()
if card_cryptogram != expected:
raise InvalidResponseError('Card cryptogram mismatch')
client_cryptogram = hashlib.sha256(shared_secret + card_challenge).digest()
response = card.send_apdu(
ins=constants.INS_PAIR,
p1=0x01,
data=client_cryptogram
)
if len(response) != 33:
raise InvalidResponseError('Unexpected response length')
pairing_index = response[0]
salt = response[1:]
pairing_key = hashlib.sha256(shared_secret + salt).digest()
return pairing_index, pairing_key