mirror of
https://github.com/polhenarejos/pico-fido.git
synced 2025-12-18 17:55:07 +08:00
Now it is possible to backup and restore the internal keys to recover a pico fido. The process is splitted in two parts: a list of 24 words and a file, which stores the security key. Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
246 lines
8.0 KiB
Python
246 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
/*
|
|
* This file is part of the Pico Fido distribution (https://github.com/polhenarejos/pico-fido).
|
|
* Copyright (c) 2022 Pol Henarejos.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
"""
|
|
|
|
import sys
|
|
import argparse
|
|
import platform
|
|
from binascii import hexlify
|
|
from words import words
|
|
|
|
try:
|
|
from fido2.ctap2.config import Config
|
|
from fido2.ctap2 import Ctap2
|
|
from fido2.hid import CtapHidDevice
|
|
from fido2.utils import bytes2int, int2bytes
|
|
except:
|
|
print('ERROR: fido2 module not found! Install fido2 package.\nTry with `pip install fido2`')
|
|
sys.exit(-1)
|
|
|
|
try:
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
from cryptography.hazmat.primitives import hashes
|
|
except:
|
|
print('ERROR: cryptography module not found! Install cryptography package.\nTry with `pip install cryptography`')
|
|
sys.exit(-1)
|
|
|
|
from enum import IntEnum
|
|
from binascii import hexlify
|
|
|
|
if (platform.system() == 'Windows'):
|
|
from secure_key import windows as skey
|
|
elif (platform.system() == 'Linux'):
|
|
from secure_key import linux as skey
|
|
elif (platform.system() == 'Darwin'):
|
|
from secure_key import macos as skey
|
|
else:
|
|
print('ERROR: platform not supported')
|
|
sys.exit(-1)
|
|
|
|
class VendorConfig(Config):
|
|
|
|
class PARAM(IntEnum):
|
|
VENDOR_COMMAND_ID = 0x01
|
|
VENDOR_AUT_KEY_AGREEMENT = 0x02
|
|
VENDOR_AUT_CT = 0x03
|
|
|
|
class CMD(IntEnum):
|
|
CONFIG_AUT = 0x03e43f56b34285e2
|
|
CONFIG_KEY_AGREEMENT = 0x1831a40f04a25ed9
|
|
CONFIG_UNLOCK = 0x54365966c9a74770
|
|
CONFIG_BACKUP = 0x6b1ede62beff0d5e
|
|
|
|
class RESP(IntEnum):
|
|
KEY_AGREEMENT = 0x01
|
|
BACKUP = 0x01
|
|
|
|
def __init__(self, ctap, pin_uv_protocol=None, pin_uv_token=None):
|
|
super().__init__(ctap, pin_uv_protocol, pin_uv_token)
|
|
|
|
def _get_key_device(self):
|
|
return skey.get_secure_key()
|
|
|
|
def _get_shared_key(self):
|
|
ret = self._call(
|
|
Config.CMD.VENDOR_PROTOTYPE,
|
|
{
|
|
VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_KEY_AGREEMENT,
|
|
},
|
|
)
|
|
peer_cose_key = ret[VendorConfig.RESP.KEY_AGREEMENT]
|
|
|
|
sk = ec.generate_private_key(ec.SECP256R1())
|
|
pn = sk.public_key().public_numbers()
|
|
pb = sk.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
|
|
key_agreement = {
|
|
1: 2,
|
|
3: -25, # Per the spec, "although this is NOT the algorithm actually used"
|
|
-1: 1,
|
|
-2: int2bytes(pn.x, 32),
|
|
-3: int2bytes(pn.y, 32),
|
|
}
|
|
|
|
x = bytes2int(peer_cose_key[-2])
|
|
y = bytes2int(peer_cose_key[-3])
|
|
pk = ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key()
|
|
shared_key = sk.exchange(ec.ECDH(), pk)
|
|
|
|
xkdf = HKDF(
|
|
algorithm=hashes.SHA256(),
|
|
length=12+32,
|
|
salt=None,
|
|
info=pb
|
|
)
|
|
kdf_out = xkdf.derive(shared_key)
|
|
key_enc = kdf_out[12:]
|
|
iv = kdf_out[:12]
|
|
return iv, key_enc, key_agreement, pb
|
|
|
|
def _send_command_key(self, cmd):
|
|
iv, key_enc, key_agreement, pb = self._get_shared_key()
|
|
|
|
chacha = ChaCha20Poly1305(key_enc)
|
|
ct = chacha.encrypt(iv, self._get_key_device(), pb)
|
|
self._call(
|
|
Config.CMD.VENDOR_PROTOTYPE,
|
|
{
|
|
VendorConfig.PARAM.VENDOR_COMMAND_ID: cmd,
|
|
VendorConfig.PARAM.VENDOR_AUT_KEY_AGREEMENT: key_agreement,
|
|
VendorConfig.PARAM.VENDOR_AUT_CT: ct
|
|
},
|
|
)
|
|
|
|
def enable_device_aut(self):
|
|
self._send_command_key(VendorConfig.CMD.CONFIG_AUT)
|
|
|
|
def unlock_device(self):
|
|
self._send_command_key(VendorConfig.CMD.CONFIG_UNLOCK)
|
|
|
|
def disable_device_aut(self):
|
|
self._call(
|
|
Config.CMD.VENDOR_PROTOTYPE,
|
|
{
|
|
VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_AUT,
|
|
},
|
|
)
|
|
|
|
def backup_save(self, filename):
|
|
ret = self._call(
|
|
Config.CMD.VENDOR_PROTOTYPE,
|
|
{
|
|
VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_BACKUP,
|
|
},
|
|
)
|
|
data = ret[VendorConfig.RESP.BACKUP]
|
|
d = int.from_bytes(skey.get_secure_key(), 'big')
|
|
with open(filename, 'wb') as fp:
|
|
fp.write(b'\x01')
|
|
fp.write(data)
|
|
pk = ec.derive_private_key(d, ec.SECP256R1())
|
|
signature = pk.sign(data, ec.ECDSA(hashes.SHA256()))
|
|
fp.write(signature)
|
|
print('Remember the following words in this order:')
|
|
for c in range(24):
|
|
coef = (d//(2048**c))%2048
|
|
print(f'{(c+1):02d} - {words[coef]}')
|
|
|
|
def backup_load(self, filename):
|
|
d = 0
|
|
if (d == 0):
|
|
for c in range(24):
|
|
word = input(f'Introduce word {(c+1):02d}: ')
|
|
while (word not in words):
|
|
word = input(f'Word not found. Please, tntroduce the correct word {(c+1):02d}: ')
|
|
coef = words.index(word)
|
|
d = d+(2048**c)*coef
|
|
|
|
pk = ec.derive_private_key(d, ec.SECP256R1())
|
|
pb = pk.public_key()
|
|
with open(filename, 'rb') as fp:
|
|
format = fp.read(1)[0]
|
|
if (format == 0x1):
|
|
data = fp.read(60)
|
|
signature = fp.read()
|
|
pb.verify(signature, data, ec.ECDSA(hashes.SHA256()))
|
|
skey.set_secure_key(pk)
|
|
self._call(
|
|
Config.CMD.VENDOR_PROTOTYPE,
|
|
{
|
|
VendorConfig.PARAM.VENDOR_COMMAND_ID: VendorConfig.CMD.CONFIG_BACKUP,
|
|
VendorConfig.PARAM.VENDOR_AUT_CT: data,
|
|
},
|
|
)
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser()
|
|
subparser = parser.add_subparsers(title="commands", dest="command")
|
|
parser_secure = subparser.add_parser('secure', help='Manages security of Pico Fido.')
|
|
parser_secure.add_argument('subcommand', choices=['enable', 'disable', 'unlock'], help='Enables, disables or unlocks the security.')
|
|
|
|
parser_backup = subparser.add_parser('backup', help='Manages the backup of Pico Fido.')
|
|
parser_backup.add_argument('subcommand', choices=['save', 'load'], help='Saves or loads a backup.')
|
|
parser_backup.add_argument('filename', help='File to save or load the backup.')
|
|
|
|
args = parser.parse_args()
|
|
return args
|
|
|
|
def secure(dev, args):
|
|
vcfg = VendorConfig(Ctap2(dev))
|
|
|
|
if (args.subcommand == 'enable'):
|
|
vcfg.enable_device_aut()
|
|
elif (args.subcommand == 'unlock'):
|
|
vcfg.unlock_device()
|
|
elif (args.subcommand == 'disable'):
|
|
vcfg.disable_device_aut()
|
|
|
|
def backup(dev, args):
|
|
vcfg = VendorConfig(Ctap2(dev))
|
|
|
|
if (args.subcommand == 'save'):
|
|
vcfg.backup_save(args.filename)
|
|
elif (args.subcommand == 'load'):
|
|
vcfg.backup_load(args.filename)
|
|
|
|
|
|
def main(args):
|
|
print('Pico Fido Tool v1.2')
|
|
print('Author: Pol Henarejos')
|
|
print('Report bugs to https://github.com/polhenarejos/pico-fido/issues')
|
|
print('')
|
|
print('')
|
|
|
|
dev = next(CtapHidDevice.list_devices(), None)
|
|
|
|
if (args.command == 'secure'):
|
|
secure(dev, args)
|
|
elif (args.command == 'backup'):
|
|
backup(dev, args)
|
|
|
|
def run():
|
|
args = parse_args()
|
|
main(args)
|
|
|
|
if __name__ == "__main__":
|
|
run()
|