mirror of
https://github.com/LedgerHQ/openpgp-card-app
synced 2024-11-09 07:10:30 +00:00
1301 lines
43 KiB
Python
1301 lines
43 KiB
Python
# -*- coding: utf-8 -*-
|
|
#*****************************************************************************
|
|
# Ledger App OpenPGP.
|
|
# (c) 2024 Ledger SAS.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#*****************************************************************************
|
|
|
|
import binascii
|
|
from datetime import datetime, timezone
|
|
import pickle
|
|
from hashlib import sha1
|
|
from typing import Optional, Tuple
|
|
from dataclasses import dataclass
|
|
from Crypto.PublicKey.RSA import construct
|
|
|
|
# pylint: disable=import-error
|
|
from smartcard.System import readers # type: ignore
|
|
from smartcard.pcsc import PCSCReader # type: ignore
|
|
from smartcard import CardConnectionDecorator # type: ignore
|
|
# pylint: enable=import-error
|
|
from gpgapp.gpgcmd import DataObject, ErrorCodes, KeyTypes, PassWord, PubkeyAlgo # type: ignore
|
|
from gpgapp.gpgcmd import KEY_OPERATIONS, KEY_TEMPLATES, USER_SALUTATION # type: ignore
|
|
|
|
|
|
APDU_MAX_SIZE: int = 0xFE
|
|
APDU_CHAINING_MODE: int = 0x10
|
|
|
|
|
|
class GPGCardExcpetion(Exception):
|
|
"""Exception handler.
|
|
|
|
Attributes:
|
|
code (int): Error code
|
|
message (str): Error message
|
|
"""
|
|
|
|
def __init__(self, code, message):
|
|
self.code = code
|
|
self.message = message
|
|
super().__init__(self.message)
|
|
|
|
|
|
@dataclass
|
|
class KeyInfo:
|
|
"""Key description information"""
|
|
|
|
attribute: bytes = b""
|
|
fingerprint: bytes = b""
|
|
ca_fingerprint: bytes = b""
|
|
cert: str = ""
|
|
date: datetime = datetime.min
|
|
uif: int = 0
|
|
key: bytes = b""
|
|
|
|
def reset(self):
|
|
"""Reset the data to the initial value"""
|
|
|
|
self.attribute = b""
|
|
self.fingerprint = b""
|
|
self.ca_fingerprint = b""
|
|
self.cert = ""
|
|
self.date = datetime.min
|
|
self.uif = 0
|
|
self.key = b""
|
|
|
|
|
|
@dataclass
|
|
class CardInfo:
|
|
"""Card description information"""
|
|
|
|
#token info
|
|
AID: str = ""
|
|
ext_length: bytes = b""
|
|
ext_capabilities: bytes = b""
|
|
histo_bytes: bytes = b""
|
|
PW_status: bytes = b""
|
|
hw_features: int = 0
|
|
|
|
#user info
|
|
name: str = ""
|
|
login: str = ""
|
|
url: str = ""
|
|
lang: str = ""
|
|
salutation: str = ""
|
|
|
|
#keys info
|
|
rsa_pub_exp: int = 0
|
|
digital_counter: int = 0
|
|
|
|
sig: KeyInfo = KeyInfo()
|
|
dec: KeyInfo = KeyInfo()
|
|
aut: KeyInfo = KeyInfo()
|
|
|
|
#private info
|
|
private_01: bytes = b""
|
|
private_02: bytes = b""
|
|
private_03: bytes = b""
|
|
private_04: bytes = b""
|
|
|
|
|
|
def reset(self):
|
|
"""Reset the data to the initial value"""
|
|
|
|
#token info
|
|
self.AID = ""
|
|
self.ext_length = b""
|
|
self.ext_capabilities = b""
|
|
self.histo_bytes = b""
|
|
self.PW_status = b""
|
|
|
|
#user info
|
|
self.name = ""
|
|
self.login = ""
|
|
self.url = ""
|
|
self.lang = ""
|
|
self.salutation = ""
|
|
|
|
#keys info
|
|
self.rsa_pub_exp = 0
|
|
self.digital_counter = 0
|
|
|
|
self.sig.reset()
|
|
self.dec.reset()
|
|
self.aut.reset()
|
|
|
|
#private info
|
|
self.private_01 = b""
|
|
self.private_02 = b""
|
|
self.private_03 = b""
|
|
|
|
|
|
class GPGCard() :
|
|
def __init__(self) -> None:
|
|
self.log: bool = False
|
|
self.connection: CardConnectionDecorator = None
|
|
self.slot_current: bytes = b"\x00"
|
|
self.slot_config: bytes = bytes(3)
|
|
self.data: CardInfo = CardInfo()
|
|
self.data.reset()
|
|
|
|
def connect(self, device: str) -> None:
|
|
"""Connect to the selected Reader
|
|
|
|
Args:
|
|
device (str): Reader device name
|
|
"""
|
|
|
|
allreaders: list = readers()
|
|
for elt in allreaders:
|
|
if str(elt).startswith(device):
|
|
reader: PCSCReader.PCSCReader = elt
|
|
self.connection = reader.createConnection()
|
|
self.connection.connect()
|
|
return
|
|
print("")
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "No Reader detected!")
|
|
|
|
|
|
def disconnect(self):
|
|
"""Connect from the selected Reader"""
|
|
|
|
return self.connection.disconnect()
|
|
|
|
|
|
############### LOG interface ###############
|
|
def log_apdu(self, log: bool) -> None:
|
|
"""Control APDU debugging display
|
|
|
|
Args:
|
|
log (bool): Activate or not the debug print
|
|
"""
|
|
|
|
self.log = log
|
|
|
|
def add_log(self, mode: str, data: bytes, sw: int = 0) -> None:
|
|
"""Print APDU content
|
|
|
|
Args:
|
|
mode (str): Indicate the Send or Recv information
|
|
data (bytes): APDU content
|
|
sw (int): Returned Status
|
|
"""
|
|
|
|
if self.log:
|
|
sw_code = f" ({sw:04x})" if mode == "recv" else ""
|
|
print(f"{mode}:{sw_code} {''.join([f'{b:02x}' for b in data])}")
|
|
|
|
|
|
############### CARD interface ###############
|
|
def select(self):
|
|
"""Send SELECT APDU command"""
|
|
|
|
apdu = binascii.unhexlify(b"00A4040006D27600012401")
|
|
return self._exchange(apdu)
|
|
|
|
|
|
def activate(self):
|
|
"""Send ACTIVATE APDU command"""
|
|
|
|
apdu = binascii.unhexlify(b"00440000")
|
|
return self._exchange(apdu)
|
|
|
|
|
|
def terminate(self):
|
|
"""Send TERMINATE APDU command"""
|
|
|
|
apdu = binascii.unhexlify(b"00E60000")
|
|
return self._exchange(apdu)
|
|
|
|
|
|
def get_log(self):
|
|
"""Send GET_LOG APDU command"""
|
|
|
|
apdu = binascii.unhexlify(b"00040000")
|
|
return self._exchange(apdu)
|
|
|
|
|
|
############### API interfaces ###############
|
|
def get_all(self) -> None:
|
|
"""Retrieve all Data Object values from the Card"""
|
|
|
|
self.data.reset()
|
|
data: Optional[bytes] = b""
|
|
b_data: bytes = b""
|
|
s_data: str = ""
|
|
|
|
self.slot_current = self._get_data(DataObject.CMD_SLOT_CUR)
|
|
self.slot_config = self._get_data(DataObject.CMD_SLOT_CFG)
|
|
|
|
self.data.AID = self._get_data(DataObject.DO_AID).hex().upper()
|
|
self.data.login = self._get_data(DataObject.DO_LOGIN).decode("utf-8")
|
|
self.data.url = self._get_data(DataObject.DO_URL).decode("utf-8")
|
|
self.data.histo_bytes = self._get_data(DataObject.DO_HIST)
|
|
self.data.hw_features = int(self._get_data(DataObject.DO_GEN_FEATURES)[0])
|
|
|
|
data = self._get_data(DataObject.DO_CARDHOLDER_DATA)
|
|
tags = self._decode_tlv(data)
|
|
if DataObject.DO_CARD_NAME in tags:
|
|
self.data.name = tags[DataObject.DO_CARD_NAME].decode("utf-8")
|
|
if DataObject.DO_CARD_SALUTATION in tags:
|
|
s_data = tags[DataObject.DO_CARD_SALUTATION].decode("utf-8")
|
|
for k,v in USER_SALUTATION.items():
|
|
if v == s_data:
|
|
self.data.salutation = k
|
|
break
|
|
|
|
if DataObject.DO_CARD_LANG in tags:
|
|
self.data.lang = tags[DataObject.DO_CARD_LANG].decode("utf-8")
|
|
|
|
data = self._get_data(DataObject.DO_APP_DATA)
|
|
tags = self._decode_tlv(data)
|
|
if DataObject.DO_EXT_LEN in tags:
|
|
self.data.ext_length = tags[DataObject.DO_EXT_LEN]
|
|
if DataObject.DO_DISCRET_DATA in tags:
|
|
b_data = tags[DataObject.DO_DISCRET_DATA]
|
|
tags = self._decode_tlv(b_data)
|
|
if DataObject.DO_EXT_CAP in tags:
|
|
self.data.ext_capabilities = tags[DataObject.DO_EXT_CAP]
|
|
if DataObject.DO_SIG_ATTR in tags:
|
|
self.data.sig.attribute = tags[DataObject.DO_SIG_ATTR]
|
|
if DataObject.DO_DEC_ATTR in tags:
|
|
self.data.dec.attribute = tags[DataObject.DO_DEC_ATTR]
|
|
if DataObject.DO_AUT_ATTR in tags:
|
|
self.data.aut.attribute = tags[DataObject.DO_AUT_ATTR]
|
|
if DataObject.DO_PW_STATUS in tags:
|
|
self.data.PW_status = tags[DataObject.DO_PW_STATUS]
|
|
|
|
data = tags.get(DataObject.DO_FINGERPRINTS)
|
|
if data:
|
|
self.data.sig.fingerprint = data[0:20]
|
|
self.data.dec.fingerprint = data[20:40]
|
|
self.data.aut.fingerprint = data[40:60]
|
|
data = tags.get(DataObject.DO_CA_FINGERPRINTS)
|
|
if data:
|
|
self.data.sig.ca_fingerprint = data[0:20]
|
|
self.data.dec.ca_fingerprint = data[20:40]
|
|
self.data.aut.ca_fingerprint = data[40:60]
|
|
data = tags.get(DataObject.DO_KEY_DATES)
|
|
if data:
|
|
dates = tags[DataObject.DO_KEY_DATES]
|
|
self._conv_date_from_bytes(KeyTypes.KEY_SIG, dates[0:4])
|
|
self._conv_date_from_bytes(KeyTypes.KEY_DEC, dates[4:8])
|
|
self._conv_date_from_bytes(KeyTypes.KEY_AUT, dates[8:12])
|
|
|
|
data = self._get_data(DataObject.CMD_RSA_EXP)
|
|
self.data.rsa_pub_exp = self._get_int(data, 4)
|
|
self.data.aut.cert = self._get_data(DataObject.DO_CERT).decode("utf-8")
|
|
self.data.dec.cert = self._get_data(DataObject.DO_CERT, True).decode("utf-8")
|
|
self.data.sig.cert = self._get_data(DataObject.DO_CERT, True).decode("utf-8")
|
|
|
|
self.data.sig.uif = int(self._get_data(DataObject.DO_UIF_SIG)[0])
|
|
self.data.dec.uif = int(self._get_data(DataObject.DO_UIF_DEC)[0])
|
|
self.data.aut.uif = int(self._get_data(DataObject.DO_UIF_AUT)[0])
|
|
|
|
data = self._get_data(DataObject.DO_SEC_TEMPL)
|
|
tags = self._decode_tlv(data)
|
|
if DataObject.DO_SIG_COUNT in tags:
|
|
b_data = tags[DataObject.DO_SIG_COUNT]
|
|
self.data.digital_counter = self._get_int(b_data, 3)
|
|
|
|
if self.data.ext_capabilities[0] & 0x08:
|
|
self.data.private_01 = self._get_data(DataObject.DO_PRIVATE_01)
|
|
self.data.private_02 = self._get_data(DataObject.DO_PRIVATE_02)
|
|
self.data.private_03 = self._get_data(DataObject.DO_PRIVATE_03)
|
|
self.data.private_04 = self._get_data(DataObject.DO_PRIVATE_04)
|
|
|
|
self.data.sig.key = self._get_data(DataObject.DO_SIG_KEY)
|
|
self.data.dec.key = self._get_data(DataObject.DO_DEC_KEY)
|
|
self.data.aut.key = self._get_data(DataObject.DO_AUT_KEY)
|
|
|
|
|
|
def backup(self, file_name: str) -> None:
|
|
"""Backup data to backup file
|
|
|
|
Args:
|
|
file_name (str): Backup filename
|
|
"""
|
|
|
|
self.get_all()
|
|
with open(file_name, mode="w+b") as f:
|
|
pickle.dump(
|
|
(self.data.AID, self.data.PW_status, self.data.rsa_pub_exp, self.data.digital_counter,
|
|
self.data.private_01, self.data.private_02,
|
|
self.data.private_03, self.data.private_04,
|
|
self.data.name, self.data.login, self.data.salutation, self.data.url, self.data.lang,
|
|
self.data.sig.key, self.data.sig.uif, self.data.sig.attribute, self.data.sig.date,
|
|
self.data.sig.fingerprint, self.data.sig.ca_fingerprint, self.data.sig.cert,
|
|
self.data.dec.key, self.data.dec.uif, self.data.dec.attribute, self.data.dec.date,
|
|
self.data.dec.fingerprint, self.data.dec.ca_fingerprint, self.data.dec.cert,
|
|
self.data.aut.key, self.data.aut.uif, self.data.aut.attribute, self.data.aut.date,
|
|
self.data.aut.fingerprint, self.data.aut.ca_fingerprint, self.data.aut.cert),
|
|
f, 2)
|
|
|
|
|
|
def restore(self, file_name: str) -> None:
|
|
"""Restore data from backup file
|
|
|
|
Args:
|
|
file_name (str): Backup filename
|
|
"""
|
|
|
|
with open(file_name, mode="r+b") as f:
|
|
(self.data.AID, self.data.PW_status, self.data.rsa_pub_exp, self.data.digital_counter,
|
|
self.data.private_01, self.data.private_02, self.data.private_03, self.data.private_04,
|
|
self.data.name, self.data.login, self.data.salutation, self.data.url, self.data.lang,
|
|
self.data.sig.key, self.data.sig.uif, self.data.sig.attribute, self.data.sig.date,
|
|
self.data.sig.fingerprint, self.data.sig.ca_fingerprint, self.data.sig.cert,
|
|
self.data.dec.key, self.data.dec.uif, self.data.dec.attribute, self.data.dec.date,
|
|
self.data.dec.fingerprint, self.data.dec.ca_fingerprint, self.data.dec.cert,
|
|
self.data.aut.key, self.data.aut.uif, self.data.aut.attribute, self.data.aut.date,
|
|
self.data.aut.fingerprint, self.data.aut.ca_fingerprint, self.data.aut.cert) = pickle.load(f)
|
|
|
|
self._put_data(DataObject.DO_AID, bytes.fromhex(self.data.AID[20:28]))
|
|
self._put_data(DataObject.DO_PW_STATUS, self.data.PW_status)
|
|
|
|
self._put_data(DataObject.DO_PRIVATE_01, self.data.private_01)
|
|
self._put_data(DataObject.DO_PRIVATE_02, self.data.private_02)
|
|
self._put_data(DataObject.DO_PRIVATE_03, self.data.private_03)
|
|
self._put_data(DataObject.DO_PRIVATE_04, self.data.private_04)
|
|
|
|
self._put_data(DataObject.DO_CARD_NAME, self.data.name.encode("utf-8"))
|
|
self._put_data(DataObject.DO_LOGIN, self.data.login.encode("utf-8"))
|
|
self._put_data(DataObject.DO_CARD_LANG, self.data.lang.encode("utf-8"))
|
|
self._put_data(DataObject.DO_URL, self.data.url.encode("utf-8"))
|
|
if len(self.data.salutation) == 0:
|
|
self._put_data(DataObject.DO_CARD_SALUTATION, b'\x30')
|
|
else:
|
|
self._put_data(DataObject.DO_CARD_SALUTATION,
|
|
bytes.fromhex(USER_SALUTATION[self.data.salutation]))
|
|
|
|
self._put_data(DataObject.DO_SIG_ATTR, self.data.sig.attribute)
|
|
self._put_data(DataObject.DO_DEC_ATTR, self.data.dec.attribute)
|
|
self._put_data(DataObject.DO_AUT_ATTR, self.data.aut.attribute)
|
|
|
|
self._put_data(DataObject.DO_UIF_SIG, self.data.sig.uif.to_bytes(2, "little"))
|
|
self._put_data(DataObject.DO_UIF_DEC, self.data.dec.uif.to_bytes(2, "little"))
|
|
self._put_data(DataObject.DO_UIF_AUT, self.data.aut.uif.to_bytes(2, "little"))
|
|
|
|
self._put_data(DataObject.DO_SIG_COUNT, self.data.digital_counter.to_bytes(4, "big"))
|
|
self._put_data(DataObject.CMD_RSA_EXP, self.data.rsa_pub_exp.to_bytes(4, "little"))
|
|
|
|
self._put_data(DataObject.DO_CERT, self.data.aut.cert.encode("utf-8"))
|
|
self._put_data(DataObject.DO_CERT, self.data.dec.cert.encode("utf-8"))
|
|
self._put_data(DataObject.DO_CERT, self.data.sig.cert.encode("utf-8"))
|
|
|
|
self._put_data(DataObject.DO_CA_FINGERPRINT_WR_SIG, self.data.sig.ca_fingerprint)
|
|
self._put_data(DataObject.DO_FINGERPRINT_WR_SIG, self.data.sig.fingerprint)
|
|
date = str(self.data.sig.date)
|
|
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
|
|
bdate = int(dt.timestamp()).to_bytes(4, "big")
|
|
self._put_data(DataObject.DO_DATES_WR_SIG, bdate)
|
|
self._put_data(DataObject.DO_SIG_KEY, self.data.sig.key)
|
|
|
|
self._put_data(DataObject.DO_CA_FINGERPRINT_WR_DEC, self.data.dec.ca_fingerprint)
|
|
self._put_data(DataObject.DO_FINGERPRINT_WR_DEC, self.data.dec.fingerprint)
|
|
date = str(self.data.dec.date)
|
|
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
|
|
bdate = int(dt.timestamp()).to_bytes(4, "big")
|
|
self._put_data(DataObject.DO_DATES_WR_DEC, bdate)
|
|
self._put_data(DataObject.DO_DEC_KEY, self.data.dec.key)
|
|
|
|
self._put_data(DataObject.DO_CA_FINGERPRINT_WR_AUT, self.data.aut.ca_fingerprint)
|
|
self._put_data(DataObject.DO_FINGERPRINT_WR_AUT, self.data.aut.fingerprint)
|
|
date = str(self.data.aut.date)
|
|
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
|
|
bdate = int(dt.timestamp()).to_bytes(4, "big")
|
|
self._put_data(DataObject.DO_DATES_WR_AUT, bdate)
|
|
self._put_data(DataObject.DO_AUT_KEY, self.data.aut.key)
|
|
|
|
|
|
def export_pub_key(self, pubkey: dict, file_name: str) -> None:
|
|
"""Export a Public to file
|
|
|
|
Args:
|
|
pubkey (dict): Public key parameters
|
|
file_name (str): Backup filename
|
|
"""
|
|
|
|
modulus = bytearray.fromhex(pubkey["Modulus"])
|
|
exponent = bytearray.fromhex(pubkey["Pub Exp"][2:])
|
|
key = construct((int.from_bytes(modulus, 'big'), int.from_bytes(exponent, 'big')))
|
|
public_key = key.publickey().export_key()
|
|
with open(file_name, mode="wb") as f:
|
|
f.write(public_key)
|
|
|
|
|
|
def seed_key(self) -> None:
|
|
"""Regenerate keys, based on seed mode"""
|
|
|
|
apdu = binascii.unhexlify(b"0047800102B600")
|
|
self._exchange(apdu)
|
|
apdu = binascii.unhexlify(b"0047800102B800")
|
|
self._exchange(apdu)
|
|
apdu = binascii.unhexlify(b"0047800102A400")
|
|
self._exchange(apdu)
|
|
|
|
|
|
############### Information decoding ###############
|
|
def decode_AID(self) -> dict:
|
|
"""Decode Application IDentity information"""
|
|
|
|
return {
|
|
"AID": f"{self.data.AID}",
|
|
"RID": f"{self.data.AID[0:10]}",
|
|
"Application": f"{self.data.AID[10:12]}",
|
|
"Version": f"{int(self.data.AID[12:14]):d}.{int(self.data.AID[14:16]):d}",
|
|
"Manufacturer": f"{self.data.AID[16:20]}",
|
|
"Serial": f"{self.data.AID[20:28]}"
|
|
}
|
|
|
|
def decode_histo(self) -> dict:
|
|
"""Decode Historical Bytes information"""
|
|
|
|
return {
|
|
"historical bytes": self.data.histo_bytes.hex()
|
|
}
|
|
|
|
def decode_extlength(self) -> dict:
|
|
"""Decode Extended Length information"""
|
|
|
|
d = {
|
|
"Command": "N/A",
|
|
"Response": "N/A",
|
|
}
|
|
|
|
if self.data.ext_length:
|
|
d["Command"] = f"{self._get_int(self.data.ext_length, offset=2):d}"
|
|
d["Response"] = f"{self._get_int(self.data.ext_length, offset=6):d}"
|
|
return d
|
|
|
|
def decode_ext_capabilities(self) -> dict:
|
|
"""Decode Extended Capabilities information"""
|
|
|
|
d = {}
|
|
b1 = self.data.ext_capabilities[0]
|
|
if b1 & 0x80:
|
|
if self.data.ext_capabilities[1] == 1:
|
|
d["Secure Messaging"] = "✓: AES 128 bits"
|
|
elif self.data.ext_capabilities[1] == 2:
|
|
d["Secure Messaging"] = "✓: AES 256 bits"
|
|
else:
|
|
d["Secure Messaging"] = "✓: ?? bits"
|
|
else:
|
|
d["Secure Messaging"] = "✗"
|
|
|
|
if b1 & 0x40:
|
|
max_val = self._get_int(self.data.ext_capabilities, offset=2)
|
|
d["Get Challenge"] = f"✓ (Max length: {max_val:d})"
|
|
else:
|
|
d["Get Challenge"] = "✗"
|
|
|
|
if b1 & 0x20:
|
|
d["Key import"] = "✓"
|
|
else:
|
|
d["Key import"] = "✗"
|
|
|
|
if b1 & 0x10:
|
|
d["PW status"] = "Changeable"
|
|
else:
|
|
d["PW status"] = "Fixed"
|
|
|
|
if b1 & 0x08:
|
|
d["Private DOs"] = "✓"
|
|
else:
|
|
d["Private DOs"] = "✗"
|
|
|
|
if b1 & 0x04:
|
|
d["Algo attributes"] = "Changeable"
|
|
else:
|
|
d["Algo attributes"] = "Fixed"
|
|
|
|
if b1 & 0x02:
|
|
d["PSO:DEC AES"] = "✓"
|
|
else:
|
|
d["PSO:DEC AES"] = "✗"
|
|
|
|
if b1 & 0x01:
|
|
d["Key Derived Format"] = "✓"
|
|
else:
|
|
d["Key Derived Format"] = "✗"
|
|
|
|
max_val = self._get_int(self.data.ext_capabilities, offset=4)
|
|
d["Max Cert len"] = f"{max_val:d}"
|
|
max_val = self._get_int(self.data.ext_capabilities, offset=6)
|
|
d["Max Special DO"] = f"{max_val:d}"
|
|
|
|
if self.data.ext_capabilities[8]:
|
|
d["PIN 2 format"] = "✓"
|
|
else:
|
|
d["PIN 2 format"] = "✗"
|
|
if self.data.ext_capabilities[9]:
|
|
d["MSE"] = "✓"
|
|
else:
|
|
d["MSE"] = "✗"
|
|
return d
|
|
|
|
def decode_pws(self) -> dict:
|
|
"""Decode Password information"""
|
|
|
|
if self.data.PW_status[0] == 0:
|
|
validity = "Only 1 PSO:CDS"
|
|
elif self.data.PW_status[0] == 1:
|
|
validity = "Several PSO:CDS"
|
|
else:
|
|
validity = f"unknown ({self.data.PW_status[0]:d})"
|
|
|
|
cfg = {
|
|
"PW1": {"format": 1, "counter": 4},
|
|
"Reset Counter": {"format": 2, "counter": 5},
|
|
"PW3": {"format": 3, "counter": 6},
|
|
}
|
|
|
|
d = {}
|
|
for name, pw in cfg.items():
|
|
if self.data.PW_status[pw["format"]] & 0x80:
|
|
fmt = "Format-2"
|
|
else:
|
|
fmt = "UTF-8"
|
|
pwlen = self.data.PW_status[pw["format"]] & 0x7f
|
|
counter = self.data.PW_status[pw['counter']]
|
|
d[name] = f"{fmt} ({pwlen:d} bytes), Error Counter={counter:d}"
|
|
if name == "PW1":
|
|
d[name] += f", Validity={validity}"
|
|
return d
|
|
|
|
def decode_hardware(self) -> dict:
|
|
"""Decode Hardware features information"""
|
|
|
|
d = {}
|
|
d["Display"] = "✓" if self.data.hw_features & 0x80 else "✗"
|
|
d["Biometric sensor"] = "✓" if self.data.hw_features & 0x40 else "✗"
|
|
d["Button/Keypad"] = "✓" if self.data.hw_features & 0x20 else "✗"
|
|
d["LED"] = "✓" if self.data.hw_features & 0x10 else "✗"
|
|
d["Loudspeaker"] = "✓" if self.data.hw_features & 0x08 else "✗"
|
|
d["Microphone"] = "✓" if self.data.hw_features & 0x04 else "✗"
|
|
d["Touchscreen"] = "✓" if self.data.hw_features & 0x02 else "✗"
|
|
d["Battery"] = "✓" if self.data.hw_features & 0x01 else "✗"
|
|
return d
|
|
|
|
|
|
############### SLOT interface ###############
|
|
def select_slot(self, slot: int) -> None:
|
|
"""Select the key slot
|
|
|
|
Args:
|
|
slot (int): slot id to select (0 to 3)
|
|
"""
|
|
|
|
self.slot_current = slot.to_bytes(1, "big")
|
|
self._put_data(DataObject.CMD_SLOT_CUR, self.slot_current)
|
|
|
|
def decode_slot(self) -> dict:
|
|
"""Decode Slots information
|
|
|
|
Returns:
|
|
Slots configuration dictionary
|
|
"""
|
|
|
|
d = {}
|
|
d["Number of Slots"] = str(self.slot_config[0])
|
|
d["Default Slot"] = str(self.slot_config[1] + 1)
|
|
d["Selection by APDU"] = "✓" if self.slot_config[2] & 0x01 else "✗"
|
|
d["Selection by screen"] = "✓" if self.slot_config[2] & 0x02 else "✗"
|
|
d["Current"] = str(int.from_bytes(self.slot_current, "big") + 1)
|
|
return d
|
|
|
|
|
|
############### USER interface ###############
|
|
def set_serial(self, serial: str) -> None:
|
|
"""Set the Card serial number
|
|
|
|
Args:
|
|
serial (str): New serial number
|
|
"""
|
|
|
|
if not self.data.AID:
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "Invalid AID!")
|
|
|
|
self.data.AID = self.data.AID[0:20] + serial
|
|
self._put_data(DataObject.DO_AID, bytes.fromhex(serial))
|
|
|
|
|
|
def set_name(self, name: str) -> None:
|
|
"""Set the Card User name
|
|
|
|
Args:
|
|
name (str): New name
|
|
"""
|
|
|
|
self.data.name = name
|
|
self._put_data(DataObject.DO_CARD_NAME, name.encode("utf-8"))
|
|
|
|
def get_name(self) -> str:
|
|
"""Get the Card User name"""
|
|
|
|
return self.data.name
|
|
|
|
|
|
def set_login(self, login: str) -> None:
|
|
"""Set the Card User login
|
|
|
|
Args:
|
|
login (str): New login
|
|
"""
|
|
|
|
self.data.login = login
|
|
self._put_data(DataObject.DO_LOGIN, login.encode("utf-8"))
|
|
|
|
def get_login(self) -> str:
|
|
"""Get the Card User login"""
|
|
|
|
return self.data.login
|
|
|
|
|
|
def set_url(self, url: str) -> None:
|
|
"""Set the Card User URL
|
|
|
|
Args:
|
|
url (str): New URL
|
|
"""
|
|
|
|
self.data.url = url
|
|
self._put_data(DataObject.DO_URL, url.encode("utf-8"))
|
|
|
|
def get_url(self) -> str:
|
|
"""Get the Card User URL"""
|
|
|
|
return self.data.url
|
|
|
|
|
|
def set_lang(self, lang: str) -> None:
|
|
"""Set the Card User language
|
|
|
|
Args:
|
|
lang (str): New language
|
|
"""
|
|
|
|
self.data.lang = lang
|
|
self._put_data(DataObject.DO_CARD_LANG, lang.encode("utf-8"))
|
|
|
|
def get_lang(self) -> str:
|
|
"""Get the Card User language"""
|
|
|
|
return self.data.lang
|
|
|
|
|
|
def set_salutation(self, salutation: str) -> None:
|
|
"""Set the Card User salutation
|
|
|
|
Args:
|
|
salutation (str): New salutation
|
|
"""
|
|
|
|
try:
|
|
salutation_str = USER_SALUTATION[salutation].encode("utf-8")
|
|
except KeyError as err:
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL,
|
|
f"Invalid salutation value ({salutation})!") from err
|
|
|
|
self.data.salutation = salutation
|
|
self._put_data(DataObject.DO_CARD_SALUTATION, salutation_str)
|
|
|
|
def get_salutation(self) -> str:
|
|
"""Get the Card User salutation"""
|
|
|
|
return self.data.salutation
|
|
|
|
|
|
############### PASSWORD interface ###############
|
|
def verify_pin(self, pw: PassWord, value: str, pinpad: bool = False) -> bool:
|
|
"""Verify the password
|
|
|
|
Args:
|
|
pw (PassWord): Password type, corresponding to User, Admin
|
|
value (str) : Password value
|
|
pinpad (bool): Indicate to use pinpad
|
|
|
|
Return:
|
|
Success / KO boolean
|
|
"""
|
|
|
|
value = value if value else ""
|
|
if pinpad:
|
|
apdu = bytes.fromhex(f"EF2000{pw:02x}00")
|
|
else:
|
|
apdu = bytes.fromhex(f"002000{pw:02x}{len(value):02x}") + value.encode("utf-8")
|
|
_, sw = self._exchange(apdu)
|
|
return sw == ErrorCodes.ERR_SUCCESS
|
|
|
|
|
|
def change_pin(self, pw: PassWord, cur_value: str, new_value: str) -> bool:
|
|
"""Update the password
|
|
|
|
Args:
|
|
pw (PassWord): Password type, corresponding to User, Admin
|
|
cur_value (str): Current password value
|
|
new_value (str): New password value
|
|
|
|
Return:
|
|
Success / KO boolean
|
|
"""
|
|
|
|
lc = len(cur_value) + len(new_value)
|
|
apdu = bytes.fromhex(f"002400{pw:02x}{lc:02x}") + \
|
|
cur_value.encode("utf-8") + \
|
|
new_value.encode("utf-8")
|
|
_, sw = self._exchange(apdu)
|
|
return sw == ErrorCodes.ERR_SUCCESS
|
|
|
|
|
|
def set_RC(self, value: str) -> bool:
|
|
"""Set the User Password Resetting Code
|
|
|
|
Args:
|
|
value (str): Resetting Code value
|
|
|
|
Return:
|
|
Success / KO boolean
|
|
"""
|
|
|
|
b_value = value.encode("utf-8")
|
|
return self._put_data(DataObject.DO_RESET_CODE, b_value) == ErrorCodes.ERR_SUCCESS
|
|
|
|
|
|
def reset_PW1(self, RC: str, value: str) -> bool:
|
|
"""Reset the User Password with Resetting Code
|
|
|
|
Args:
|
|
RC (str): Resetting Code value
|
|
value (str): User Password value
|
|
|
|
Return:
|
|
Success / KO boolean
|
|
"""
|
|
|
|
p1 = 2 if len(RC) == 0 else 0
|
|
lc = len(RC) + len(value)
|
|
apdu = bytes.fromhex(f"002C{p1:02x}81{lc:02x}") + RC.encode("utf-8") + value.encode("utf-8")
|
|
_, sw = self._exchange(apdu)
|
|
return sw == ErrorCodes.ERR_SUCCESS
|
|
|
|
|
|
############### KEYS interface ###############
|
|
def decode_key_uif(self, key: str) -> str:
|
|
"""Decode the selected key User Interaction Flag
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
|
|
Return:
|
|
UIF status for the selected key
|
|
"""
|
|
|
|
uif = self._get_key_object(key).uif
|
|
if uif == 0:
|
|
return "✗"
|
|
if uif == 1:
|
|
return "✓"
|
|
if uif == 2:
|
|
return "✓ (Permanent)"
|
|
return ""
|
|
|
|
def get_key_date(self, key: str) -> str:
|
|
"""Get key Creation Date
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
|
|
Return:
|
|
Key Creation Date
|
|
"""
|
|
|
|
return str(self._get_key_object(key).date)
|
|
|
|
def get_sig_count(self) -> int:
|
|
"""Get Digital Signatures Count
|
|
|
|
Return:
|
|
Number of Digital Signatures Count
|
|
"""
|
|
|
|
return self.data.digital_counter
|
|
|
|
def get_rsa_pub_exp(self) -> int:
|
|
"""Get RSA Public Exponent
|
|
|
|
Return:
|
|
RSA Public Exponent
|
|
"""
|
|
|
|
return self.data.rsa_pub_exp
|
|
|
|
def get_key_cert(self, key: str) -> str:
|
|
"""Get key Certificate
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
|
|
Return:
|
|
Key Certificate
|
|
"""
|
|
|
|
return self._get_key_object(key).cert
|
|
|
|
def set_template(self, key: str, template: str) -> None:
|
|
"""Set key Template
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
template (str): Key template
|
|
"""
|
|
|
|
if template not in KEY_TEMPLATES:
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid template: {template}")
|
|
|
|
data = binascii.unhexlify(KEY_TEMPLATES[template])
|
|
if key == KeyTypes.KEY_SIG:
|
|
self._put_data(DataObject.DO_SIG_ATTR, data)
|
|
elif key == KeyTypes.KEY_DEC:
|
|
self._put_data(DataObject.DO_DEC_ATTR, data)
|
|
elif key == KeyTypes.KEY_AUT:
|
|
self._put_data(DataObject.DO_AUT_ATTR, data)
|
|
|
|
def set_key_fingerprint(self, key: str, data: bytes) -> None:
|
|
"""Set key fingerprint
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
data (bytes): Fingerprint
|
|
"""
|
|
|
|
if key == KeyTypes.KEY_SIG:
|
|
self.data.sig.fingerprint = data
|
|
self._put_data(DataObject.DO_FINGERPRINT_WR_SIG, data)
|
|
elif key == KeyTypes.KEY_AUT:
|
|
self.data.aut.fingerprint = data
|
|
self._put_data(DataObject.DO_FINGERPRINT_WR_AUT, data)
|
|
elif key == KeyTypes.KEY_DEC:
|
|
self.data.dec.fingerprint = data
|
|
self._put_data(DataObject.DO_FINGERPRINT_WR_DEC, data)
|
|
|
|
def get_key_fingerprint(self, key: str) -> str:
|
|
"""Get key fingerprint
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
|
|
Return:
|
|
Key Fingerprint
|
|
"""
|
|
|
|
fingerprint = self._get_key_object(key).fingerprint
|
|
sdata = binascii.hexlify(fingerprint).decode("ascii")
|
|
return sdata if sdata != "0"*40 else "N/A"
|
|
|
|
def get_key_CA_fingerprint(self, key: str) -> str:
|
|
"""Get key CA fingerprint
|
|
|
|
Args:
|
|
ey (str): Key type (SIG, DC, AUT)
|
|
|
|
Return:
|
|
Key CA Fingerprint
|
|
"""
|
|
|
|
fingerprint = self._get_key_object(key).ca_fingerprint
|
|
sdata = binascii.hexlify(fingerprint).decode("ascii")
|
|
return sdata if sdata != "0"*40 else "N/A"
|
|
|
|
def decode_attributes(self, key: str) -> str:
|
|
"""Decode key attribute
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
|
|
Return:
|
|
String with attributes and size
|
|
"""
|
|
|
|
attributes = self._get_key_object(key).attribute
|
|
if not attributes or len(attributes) == 0:
|
|
return ""
|
|
|
|
if attributes[0] not in set(iter(PubkeyAlgo)):
|
|
return ""
|
|
|
|
if attributes[0] == PubkeyAlgo.RSA:
|
|
if attributes[5] == 0:
|
|
fmt = "standard (e, p, q)"
|
|
elif attributes[5] == 1:
|
|
fmt = "standard with modulus (n)"
|
|
elif attributes[5] == 2:
|
|
fmt = "crt (Chinese Remainder Theorem)"
|
|
elif attributes[5] == 3:
|
|
fmt = "crt (Chinese Remainder Theorem) with modulus (n)"
|
|
ret = f"RSA-{self._get_int(attributes, offset=1)}"
|
|
ret += f", Format: {fmt}"
|
|
ret += f", Exponent size: {self._get_int(attributes, offset=3)}"
|
|
return ret
|
|
|
|
if attributes[0] == PubkeyAlgo.ECDSA:
|
|
ret = "ECDSA"
|
|
if attributes[0] == PubkeyAlgo.ECDH:
|
|
ret = "ECDH"
|
|
if attributes[0] == PubkeyAlgo.EDDSA:
|
|
ret = "EDDSA"
|
|
else:
|
|
ret = ""
|
|
return ret
|
|
|
|
|
|
def decode_key(self, key: str) -> dict:
|
|
"""Get key parameters
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
|
|
Return:
|
|
Key information dictionary
|
|
"""
|
|
|
|
d = {}
|
|
offset: int = 0
|
|
key_data = self._get_key_object(key).key
|
|
|
|
d["OS Target ID"] = f"0x{int.from_bytes(key_data[offset:offset + 4], 'big'):04x}"
|
|
offset += 4
|
|
d["API Level"] = str(int.from_bytes(key_data[offset:offset + 4], 'big'))
|
|
offset += 4
|
|
size = int.from_bytes(key_data[offset:offset + 4], 'big')
|
|
# Should be Public key here from doc, but only Public Exp from the code
|
|
d["Public exp size"] = str(size)
|
|
offset += 4
|
|
d["Public exp"] = f"0x{int.from_bytes(key_data[offset:offset + 4], 'big'):06x}"
|
|
offset += size
|
|
size = int.from_bytes(key_data[offset:offset + 4], 'big')
|
|
d["Private key size"] = str(size)
|
|
# offset += 4
|
|
# d["Private key encrypted"] = key_data[offset:offset + size].hex()
|
|
return d
|
|
|
|
def asymmetric_key(self, key: str, action: str) -> dict:
|
|
"""Asymmetric key operation
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
action (str): Generate or Read
|
|
|
|
Return:
|
|
Public key information:
|
|
RSA (Encrypt or Sign): {
|
|
"id": (int) 1,
|
|
"Modulus": (int)
|
|
"Pub Exp" (int)
|
|
}
|
|
ECDSA for PSO:CDS and INT-AUT: {
|
|
"id": (int) 19,
|
|
"OID": (bytes)
|
|
}
|
|
ECDH for PSO:DEC {
|
|
"id": (int) 18,
|
|
"OID": (bytes)
|
|
}
|
|
"""
|
|
|
|
if action not in KEY_OPERATIONS:
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid Key operation: {action}")
|
|
|
|
op = KEY_OPERATIONS[action]
|
|
attributes = None
|
|
if key == KeyTypes.KEY_SIG:
|
|
attributes = self.data.sig.attribute
|
|
b_key = DataObject.DO_SIG_KEY
|
|
elif key == KeyTypes.KEY_DEC:
|
|
attributes = self.data.dec.attribute
|
|
b_key = DataObject.DO_DEC_KEY
|
|
elif key == KeyTypes.KEY_AUT:
|
|
attributes = self.data.aut.attribute
|
|
b_key = DataObject.DO_AUT_KEY
|
|
if not attributes or len(attributes) == 0:
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "Invalid key attribute!")
|
|
|
|
if attributes[0] not in set(iter(PubkeyAlgo)):
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "Invalid key ID in attribute!")
|
|
|
|
d = {}
|
|
tags = self._asym_key_pair(op, b_key)
|
|
|
|
if attributes[0] == PubkeyAlgo.RSA:
|
|
d["ID"] = f"RSA-{self._get_int(attributes, offset=1)}"
|
|
d["Modulus"] = tags[0x81].hex()
|
|
d["Pub Exp"] = f"0x{tags[0x82].hex()}"
|
|
else:
|
|
if attributes[0] == PubkeyAlgo.ECDSA:
|
|
d["ID"] = "ECDSA"
|
|
if attributes[0] == PubkeyAlgo.ECDH:
|
|
d["ID"] = "ECDH"
|
|
if attributes[0] == PubkeyAlgo.EDDSA:
|
|
d["ID"] = "EDDSA"
|
|
d["oid"] = tags[0x86].hex()
|
|
|
|
if attributes[0] == PubkeyAlgo.RSA:
|
|
if action == "Generate":
|
|
# Set the generation date
|
|
self._set_key_date_now(key)
|
|
|
|
# Get the generation date
|
|
date = self.get_key_date(key)
|
|
dt = datetime.strptime(date, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
|
|
kdate = int(dt.timestamp())
|
|
d["Creation date"] = date
|
|
|
|
# Compute the fingerprint https://www.rfc-editor.org/rfc/rfc4880#section-12.2
|
|
modulus: bytes = bytes.fromhex(d["Modulus"])
|
|
ksize: int = self._get_int(attributes, offset=1)
|
|
size: int = int(ksize / 8) + 0x0D # len(header + tag + pub exp)
|
|
|
|
header: bytes = bytes.fromhex(f"99{size:04x}04{kdate:08x}01{ksize:04x}")
|
|
footer: bytes = bytes.fromhex("0011010001")
|
|
data: bytes = header + modulus + footer
|
|
|
|
_hash = sha1()
|
|
_hash.update(data)
|
|
result: bytes = _hash.digest()
|
|
d["Fingerprint"] = result.hex()
|
|
if action == "Generate":
|
|
self.set_key_fingerprint(key, result)
|
|
|
|
return d
|
|
|
|
|
|
# ===============================================================
|
|
# Internal functions
|
|
# ===============================================================
|
|
|
|
def _get_key_object(self, key: str) -> KeyInfo:
|
|
"""Get KeyInfo data class object
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
|
|
Return:
|
|
KeyInfo Object
|
|
"""
|
|
|
|
if key == KeyTypes.KEY_SIG:
|
|
return self.data.sig
|
|
if key == KeyTypes.KEY_AUT:
|
|
return self.data.aut
|
|
if key == KeyTypes.KEY_DEC:
|
|
return self.data.dec
|
|
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, f"Invalid key type {key}!")
|
|
|
|
|
|
def _get_data(self, tag: int, bnext: bool = False) -> bytes:
|
|
"""Send APDU command to GET a specific Data Object
|
|
|
|
Args:
|
|
tag (int): Data Object tag
|
|
|
|
Return:
|
|
Data Object bytes
|
|
"""
|
|
|
|
ins = 0xCC if bnext else 0xCA
|
|
apdu = bytes.fromhex(f"00{ins:02x}{tag:04x}00")
|
|
resp, _ = self._exchange(apdu)
|
|
return resp
|
|
|
|
|
|
def _put_data(self, tag: int, data: bytes) -> int:
|
|
"""Send APDU command to PUT a Data Object value
|
|
|
|
Args:
|
|
tag (int): Data Object tag
|
|
data (bytes): Data Object bytes
|
|
|
|
Return:
|
|
Status Word
|
|
"""
|
|
|
|
apdu = bytes.fromhex(f"00DA{tag:04x}")
|
|
_, sw = self._exchange(apdu, data)
|
|
return sw
|
|
|
|
|
|
def _asym_key_pair(self, op: int, key: int) -> dict:
|
|
"""Asymmetric key pair operation
|
|
|
|
Args:
|
|
op (int): Operation to execute
|
|
key (int): Key type
|
|
|
|
Return:
|
|
Public key decoded data bytes
|
|
"""
|
|
|
|
apdu = bytes.fromhex(f"0047{op:02x}0002{key:02x}00")
|
|
resp, sw = self._exchange(apdu)
|
|
if sw != ErrorCodes.ERR_SUCCESS:
|
|
raise GPGCardExcpetion(ErrorCodes.ERR_INTERNAL, "Key operation error!")
|
|
tags = self._decode_tlv(resp)
|
|
return self._decode_tlv(tags[DataObject.DO_PUB_KEY])
|
|
|
|
|
|
def _conv_date_from_bytes(self, key: str, data: bytes) -> None:
|
|
"""Convert date from bytes to datetime local dataclass
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
data (bytes): Date value
|
|
"""
|
|
|
|
idate = int.from_bytes(data, "big")
|
|
self._get_key_object(key).date = datetime.utcfromtimestamp(idate)
|
|
|
|
|
|
def _set_key_date_now(self, key: str) -> None:
|
|
"""Set Key creation date
|
|
|
|
Args:
|
|
key (str): Key type (SIG, DC, AUT)
|
|
"""
|
|
|
|
dt = datetime.utcnow().replace(microsecond=0)
|
|
bdate = int(dt.timestamp()).to_bytes(4, "big")
|
|
if key == KeyTypes.KEY_SIG:
|
|
self.data.sig.date = dt
|
|
tag = DataObject.DO_DATES_WR_SIG
|
|
elif key == KeyTypes.KEY_AUT:
|
|
self.data.aut.date = dt
|
|
tag = DataObject.DO_DATES_WR_AUT
|
|
elif key == KeyTypes.KEY_DEC:
|
|
self.data.dec.date = dt
|
|
tag = DataObject.DO_DATES_WR_DEC
|
|
self._put_data(tag, bdate)
|
|
|
|
|
|
def _decode_tlv(self, tlv: bytes) -> dict:
|
|
"""Decode TLV fields
|
|
|
|
Args:
|
|
tlv (bytes): Input data bytes to parse
|
|
|
|
Returns:
|
|
dict {t: v, t:v, ...}
|
|
"""
|
|
|
|
tags = {}
|
|
while len(tlv):
|
|
o = 0
|
|
l = 0
|
|
if (tlv[0] & 0x1F) == 0x1F:
|
|
t = self._get_int(tlv)
|
|
o = 2
|
|
else:
|
|
t = tlv[0]
|
|
o = 1
|
|
l = tlv[o]
|
|
if l & 0x80 :
|
|
if (l & 0x7f) == 1:
|
|
l = tlv[o + 1]
|
|
o += 2
|
|
if (l & 0x7f) == 2:
|
|
l = self._get_int(tlv, offset=o + 1)
|
|
o += 3
|
|
else:
|
|
o += 1
|
|
v = tlv[o:o + l]
|
|
tags[t] = v
|
|
tlv = tlv[o + l:]
|
|
return tags
|
|
|
|
|
|
def _transmit(self, data: bytes, long_resp: bool = False) -> Tuple[bytes, int, int]:
|
|
"""Transmit data, and get the response
|
|
|
|
Args:
|
|
data (bytes): APDU to transmit
|
|
long_resp (bool): Indicate if long response is expected
|
|
|
|
Return:
|
|
Response data bytes and the Status Word
|
|
"""
|
|
|
|
self.add_log("send", data)
|
|
resp, sw1, sw2 = self.connection.transmit(list(data))
|
|
sw = (sw1 << 8) | sw2
|
|
self.add_log("recv", resp, sw)
|
|
if sw != ErrorCodes.ERR_SUCCESS and not long_resp:
|
|
raise GPGCardExcpetion(sw, "")
|
|
return resp, sw1, sw2
|
|
|
|
|
|
def _exchange(self, apdu: bytes, data: bytes = b"") -> Tuple[bytes, int]:
|
|
"""Exchange APDU, and get the response
|
|
|
|
Args:
|
|
apdu (bytes): APDU content
|
|
data (bytes): Data to transmit
|
|
|
|
Return:
|
|
Response data bytes and the Status Word
|
|
"""
|
|
|
|
#send
|
|
apdux: bytes = b""
|
|
resp: bytes = b""
|
|
if len(data) > 0:
|
|
if len(data) > APDU_MAX_SIZE:
|
|
cla: bytes = (apdu[0] | APDU_CHAINING_MODE).to_bytes(1, "big")
|
|
m_apdu: bytes = cla + apdu[1:5] + APDU_MAX_SIZE.to_bytes(1, "big")
|
|
while len(data) > APDU_MAX_SIZE:
|
|
apdux = m_apdu + data[0:APDU_MAX_SIZE]
|
|
self._transmit(apdux)
|
|
data = data[APDU_MAX_SIZE:]
|
|
apdu += len(data).to_bytes(1, "big") + data
|
|
|
|
resp, sw1, sw2 = self._transmit(apdu, True)
|
|
|
|
#receive
|
|
while sw1 == ErrorCodes.ERR_SW1_VALID:
|
|
apdux = bytes.fromhex(f"00c00000{sw2:02x}")
|
|
resp2, sw1, sw2 = self._transmit(apdux, True)
|
|
resp += resp2
|
|
sw = (sw1 << 8) | sw2
|
|
return bytes(resp), sw
|
|
|
|
|
|
def _get_int(self, buffer: bytes, size: int = 2, offset: int = 0) -> int:
|
|
"""Exchange APDU, and get the response
|
|
|
|
Args:
|
|
buffer (bytes): data content
|
|
offset (int): Offset of MSB
|
|
|
|
Return:
|
|
Converted int
|
|
"""
|
|
|
|
if size == 2:
|
|
return (buffer[offset] << 8) | buffer[offset + 1]
|
|
if size == 3:
|
|
return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2]
|
|
if size == 4:
|
|
return (buffer[offset] << 24) | (buffer[offset + 1] << 16) | \
|
|
(buffer[offset + 2] << 8) | buffer[offset + 3]
|
|
return 0
|