Fix windows timeout issues. Fix reset command. Fix wrong XML length

This commit is contained in:
Bjoern Kerler 2019-12-08 10:14:52 +01:00
parent e564b9786e
commit 8f6c70746d
7 changed files with 521 additions and 12 deletions

Binary file not shown.

Binary file not shown.

487
Library/cryptutils.py Normal file
View file

@ -0,0 +1,487 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#(c) B.Kerler 2018-2019
import hashlib
from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto.Hash import CMAC
from Crypto.Util.number import long_to_bytes, bytes_to_long
from binascii import hexlify,unhexlify
class PKCS1BaseException(Exception):
pass
class DecryptionError(PKCS1BaseException):
pass
class MessageTooLong(PKCS1BaseException):
pass
class WrongLength(PKCS1BaseException):
pass
class MessageTooShort(PKCS1BaseException):
pass
class InvalidSignature(PKCS1BaseException):
pass
class RSAModulusTooShort(PKCS1BaseException):
pass
class IntegerTooLarge(PKCS1BaseException):
pass
class MessageRepresentativeOutOfRange(PKCS1BaseException):
pass
class CiphertextRepresentativeOutOfRange(PKCS1BaseException):
pass
class SignatureRepresentativeOutOfRange(PKCS1BaseException):
pass
class EncodingError(PKCS1BaseException):
pass
class InvalidInputException(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return str(self.msg)
class InvalidTagException(Exception):
def __str__(self):
return 'The authentication tag is invalid.'
class cryptutils:
class aes:
# GF(2^128) defined by 1 + a + a^2 + a^7 + a^128
# Please note the MSB is x0 and LSB is x127
def gf_2_128_mul(self, x, y):
assert x < (1 << 128)
assert y < (1 << 128)
res = 0
for i in range(127, -1, -1):
res ^= x * ((y >> i) & 1) # branchless
x = (x >> 1) ^ ((x & 1) * 0xE1000000000000000000000000000000)
assert res < 1 << 128
return res
class AES_GCM:
# Galois/Counter Mode with AES-128 and 96-bit IV
'''
Example:
master_key = 0x0ADAABC70895E008147A48C27791F654 #54F69177C2487A1408E09508C7ABDA0A
init_value = 0x2883B4173F9A838437C1CD86CCFAA5ED #EDA5FACC86CDC13784839A3F17B48328
auth_tag = 46D1FA806ADA1A916E6D0D0B55A40C1F94D7820D110F3DFC984AA3EEC9D67521
ciphertext = b"\x8A\x40\x9D\xF8\x76\x09\xCA\x10\x36\xB3\xFA\x86\x20\xC5\x85\xA3"+ \
b"\xE3\x8E\x17\x14\x40\xBD\x6B\xA7\x26\x1F\x0B\xFE\xC5\x0A\xB0\xF0"+\
b"\xCF\x69\x2E\x76\x18\x6D\x96\x9E\x83\x87\x63\xC7\x15\x7C\x1F\x28"+\
b"\xEE\xE8\xF1\xD6\x1F\x02\x2A\xF1\xA2\x43\x8A\xCF\x7C\xF2\x66\x37"+\
b"\x8B\x49\x1D\xC5\xDC\xE2\x54\x77\xED\x2F\x17\x5B\xA9\xFC\x8A\x81"+\
b"\x60\xF6\x5A\x22\x39\xCA\x79\x32\x9B\xDB\x49\x50\xCE\x74\x2C\x56"+\
b"\xDB\x97\xCA\x13\xDD\x25\xA3\x3C\x0F\x53\xDD\x38\xBF\x7B\x8B\xDA"+\
b"\xD6\x74\x38\x87\x96\xA8\x10\x5A\x96\x38\x39\x7F\xFD\xEC\xC7\x62"+\
b"\x06\x44\xF4\x0F\x78\xD6\x3D\x1A\xC5\x40\x4B\x3B\x8C\xBE\xE6\x76"+\
b"\x65\xFA\x40\xDA\xD3\xF0\xF2\x19\x35\xB7\xB2\x91\xFC\x18\x2C\x53"+\
b"\xA2\x3F\x1A\xA7\x4F\xFC\x42\xAE\xC1\x97\x89\xAB\x7E\x9B\xA1\x5C"+\
b"\x3A\x3B\x2F\x01\x60\xB1\xC5\x30\x7C\xB7\x2B\xD5\xAF\x27\xA0\x4C"+\
b"\xE9\x80\xC5\xB4\xEC\xFB\xD7\x59\xE8\x5D\xEE\xB5\x6F\x3B\xA7\xDE"+\
b"\xDA\xD8\x55\x09\x7A\x5A\xAD\x6C\x13\x2D\xD1\x23\x7C\x13\x5F\x84"+\
b"\x35\x29\x51\x55\xF4\x53\x12\x9C\x86\x7A\x77\x2B\xE2\x7B\x01\xA2"+\
b"\x6B\xC8\x5D\xD8\xCA\x92\xFB\x32\x0A\x09\xAE\xB3\x45\x8D\x0B\x60"+\
b"\x9D\xEB\xB7\x02\x07\xAB\x4A\x24\xF6\xA1\xE7\x59\xA0\xC4\xB1\xFB"+\
b"\x44\xAD\x32\xC7\xD4\x8F\xC6\x0C\x33\xD5\x88\x82\xF4\x9A\xA2\x7C"+\
b"\xDC\x56\x90\x96\x3C\xBC\xCF\x95\x17\x22\x55\x64\x67\x62\x52\x86"+\
b"\xFA\x3B\xFC\xAA\xC7\x1B\xDE\x7F\x01\xB3\x61\x8C\x28\xAE\x64\x7E"+\
b"\x43\xF0\x5A\x50\x60\x50\x85\xD4\xC4\xA6\x92\xC7\x8B\xE5\x04\x80"+\
b"\x74\x0F\xBA\xEB\x7C\x2C\x81\x07\x99\x22\x51\xD1\x9E\xE1\x59\xEE"+\
b"\x77\xC2\x13\x2C\x46\x16\x92\x9A\x69\xD9\x01\x75\x31\xA6\x20\xB9"+\
b"\x13\x46\x55\xF7\x8C\xC6\xB8\x7C\x8F\xAC\x00\x1A\x58\x68\xC7\xAD"+\
b"\x4E\x34\xB9\xEF\x5F\xCD\x87\x12\x0E\x8A\xEA\xD2\x4D\x66\x5E\x40"+\
b"\xBD\x1D\x30\x8A\x83\xB8\x4F\xC2\xAB\x28\x58\x6C\xEA\xDB\xF5\x87"+\
b"\xA0\x62\x9E\xF9\xF4\xE7\xE8\x65"
my_gcm = AES_GCM(master_key)
decrypted = my_gcm.decrypt(init_value, ciphertext, auth_tag)
'''
def __init__(self, master_key):
self.change_key(master_key)
def change_key(self, master_key):
if master_key >= (1 << 128):
raise InvalidInputException('Master key should be 128-bit')
self.__master_key = long_to_bytes(master_key, 16)
self.__aes_ecb = AES.new(self.__master_key, AES.MODE_ECB)
self.__auth_key = bytes_to_long(self.__aes_ecb.encrypt(b'\x00' * 16))
# precompute the table for multiplication in finite field
table = [] # for 8-bit
for i in range(16):
row = []
for j in range(256):
row.append(self.gf_2_128_mul(self.__auth_key, j << (8 * i)))
table.append(tuple(row))
self.__pre_table = tuple(table)
self.prev_init_value = None # reset
def __times_auth_key(self, val):
res = 0
for i in range(16):
res ^= self.__pre_table[i][val & 0xFF]
val >>= 8
return res
def __ghash(self, aad, txt):
len_aad = len(aad)
len_txt = len(txt)
# padding
if 0 == len_aad % 16:
data = aad
else:
data = aad + b'\x00' * (16 - len_aad % 16)
if 0 == len_txt % 16:
data += txt
else:
data += txt + b'\x00' * (16 - len_txt % 16)
tag = 0
assert len(data) % 16 == 0
for i in range(len(data) // 16):
tag ^= bytes_to_long(data[i * 16: (i + 1) * 16])
tag = self.__times_auth_key(tag)
# print 'X\t', hex(tag)
tag ^= ((8 * len_aad) << 64) | (8 * len_txt)
tag = self.__times_auth_key(tag)
return tag
def encrypt(self, init_value, plaintext, auth_data=b''):
if init_value >= (1 << 96):
raise InvalidInputException('IV should be 96-bit')
# a naive checking for IV reuse
if init_value == self.prev_init_value:
raise InvalidInputException('IV must not be reused!')
self.prev_init_value = init_value
len_plaintext = len(plaintext)
# len_auth_data = len(auth_data)
if len_plaintext > 0:
counter = Counter.new(
nbits=32,
prefix=long_to_bytes(init_value, 12),
initial_value=2, # notice this
allow_wraparound=False)
aes_ctr = AES.new(self.__master_key, AES.MODE_CTR, counter=counter)
if 0 != len_plaintext % 16:
padded_plaintext = plaintext + \
b'\x00' * (16 - len_plaintext % 16)
else:
padded_plaintext = plaintext
ciphertext = aes_ctr.encrypt(padded_plaintext)[:len_plaintext]
else:
ciphertext = b''
auth_tag = self.__ghash(auth_data, ciphertext)
# print 'GHASH\t', hex(auth_tag)
auth_tag ^= bytes_to_long(self.__aes_ecb.encrypt(
long_to_bytes((init_value << 32) | 1, 16)))
# assert len(ciphertext) == len(plaintext)
assert auth_tag < (1 << 128)
return ciphertext, auth_tag
def decrypt(self, init_value, ciphertext, auth_tag, auth_data=b''):
# if init_value >= (1 << 96):
# raise InvalidInputException('IV should be 96-bit')
# if auth_tag >= (1 << 128):
# raise InvalidInputException('Tag should be 128-bit')
if auth_tag != self.__ghash(auth_data, ciphertext) ^ \
bytes_to_long(self.__aes_ecb.encrypt(
long_to_bytes((init_value << 32) | 1, 16))):
raise InvalidTagException
len_ciphertext = len(ciphertext)
if len_ciphertext > 0:
counter = Counter.new(
nbits=32,
prefix=long_to_bytes(init_value, 12),
initial_value=2,
allow_wraparound=True)
aes_ctr = AES.new(self.__master_key, AES.MODE_CTR, counter=counter)
if 0 != len_ciphertext % 16:
padded_ciphertext = ciphertext + \
b'\x00' * (16 - len_ciphertext % 16)
else:
padded_ciphertext = ciphertext
plaintext = aes_ctr.decrypt(padded_ciphertext)[:len_ciphertext]
else:
plaintext = b''
return plaintext
def aes_gcm(self,ciphertext,nounce,aes_key, hdr, tag_auth, decrypt=True):
cipher = AES.new(aes_key, AES.MODE_GCM, nounce)
if hdr!=None:
cipher.update(hdr)
if decrypt:
plaintext = cipher.decrypt(ciphertext)
try:
cipher.verify(tag_auth)
return plaintext
except ValueError:
return None
def aes_cbc(self,key,iv,data,decrypt=True):
if decrypt:
return AES.new(key, AES.MODE_CBC, IV=iv).decrypt(data)
else:
return AES.new(key, AES.MODE_CBC, IV=iv).encrypt(data)
def aes_ecb(self,key,data,decrypt=True):
if decrypt:
return AES.new(key, AES.MODE_ECB).decrypt(data)
else:
return AES.new(key, AES.MODE_ECB).encrypt(data)
def aes_ctr(self,key,counter,enc_data,decrypt=True):
ctr = Counter.new(128, initial_value=counter)
# Create the AES cipher object and decrypt the ciphertext, basically this here is just aes ctr 256 :)
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
data = b""
if decrypt:
data = cipher.decrypt(enc_data)
else:
data = cipher.encrypt(enc_data)
return data
def aes_ccm(self, key, nounce, tag_auth, data, decrypt=True):
cipher = AES.new(key, AES.MODE_CCM, nounce)
if decrypt:
plaintext = cipher.decrypt(data)
try:
cipher.verify(tag_auth)
return plaintext
except ValueError:
return None
else:
ciphertext = cipher.encrypt(data)
return ciphertext
def aes_cmac_verify(key,plain,compare):
ctx = CMAC.new(key, ciphermod=AES)
ctx.update(plain)
result=ctx.hexdigest()
if result!=compare:
print("AES-CMAC failed !")
else:
print("AES-CMAC ok !")
class rsa: # RFC8017
def __init__(self, hashtype="SHA256"):
hh=hash()
if hashtype == "SHA1":
self.hash = hh.sha1
self.digestLen = 0x14
elif hashtype == "SHA256":
self.hash = hh.sha256
self.digestLen = 0x20
def pss_test(self):
N = "a2ba40ee07e3b2bd2f02ce227f36a195024486e49c19cb41bbbdfbba98b22b0e577c2eeaffa20d883a76e65e394c69d4b3c05a1e8fadda27edb2a42bc000fe888b9b32c22d15add0cd76b3e7936e19955b220dd17d4ea904b1ec102b2e4de7751222aa99151024c7cb41cc5ea21d00eeb41f7c800834d2c6e06bce3bce7ea9a5"
e = "010001"
D = "050e2c3e38d886110288dfc68a9533e7e12e27d2aa56d2cdb3fb6efa990bcff29e1d2987fb711962860e7391b1ce01ebadb9e812d2fbdfaf25df4ae26110a6d7a26f0b810f54875e17dd5c9fb6d641761245b81e79f8c88f0e55a6dcd5f133abd35f8f4ec80adf1bf86277a582894cb6ebcd2162f1c7534f1f4947b129151b71"
MSG = "859eef2fd78aca00308bdc471193bf55bf9d78db8f8a672b484634f3c9c26e6478ae10260fe0dd8c082e53a5293af2173cd50c6d5d354febf78b26021c25c02712e78cd4694c9f469777e451e7f8e9e04cd3739c6bbfedae487fb55644e9ca74ff77a53cb729802f6ed4a5ffa8ba159890fc"
salt = "e3b5d5d002c1bce50c2b65ef88a188d83bce7e61"
N = int(N, 16)
e = int(e, 16)
D = int(D, 16)
MSG = unhexlify(MSG)
salt = unhexlify(salt)
signature = self.pss_sign(D, N, self.hash(MSG), salt, 1024) # pkcs_1_pss_encode_sha256
isvalid = self.pss_verify(e, N, self.hash(MSG), signature, 1024)
if isvalid:
print("Test passed.")
else:
print("Test failed.")
def i2osp(self, x, x_len):
'''Converts the integer x to its big-endian representation of length
x_len.
'''
if x > 256 ** x_len:
raise IntegerTooLarge
h = hex(x)[2:]
if h[-1] == 'L':
h = h[:-1]
if len(h) & 1 == 1:
h = '0%s' % h
x = unhexlify(h)
return b'\x00' * int(x_len - len(x)) + x
def os2ip(self,x):
'''Converts the byte string x representing an integer reprented using the
big-endian convient to an integer.
'''
h = hexlify(x)
return int(h, 16)
#def os2ip(self, X):
# return int.from_bytes(X, byteorder='big')
def mgf1(self, input, length):
counter = 0
output = b''
while (len(output) < length):
C = self.i2osp(counter, 4)
output += self.hash(input + C)
counter += 1
return output[:length]
def assert_int(self, var: int, name: str):
if isinstance(var, int):
return
raise TypeError('%s should be an integer, not %s' % (name, var.__class__))
def sign(self,tosign,D,N,emBits=1024):
self.assert_int(tosign, 'message')
self.assert_int(D, 'D')
self.assert_int(N, 'n')
if tosign < 0:
raise ValueError('Only non-negative numbers are supported')
if tosign > N:
tosign1=divmod(tosign,N)[1]
signature=pow(tosign1,D,N)
raise OverflowError("The message %i is too long for n=%i" % (tosign, N))
signature = pow(tosign, D, N)
hexsign = self.i2osp(signature, emBits // 8)
return hexsign
def pss_sign(self, D, N, msghash, salt, emBits=1024):
if isinstance(D,str):
D=unhexlify(D)
D=self.os2ip(D)
if isinstance(N,str):
N=unhexlify(N)
N=self.os2ip(N)
slen=len(salt)
emLen = self.ceil_div(emBits, 8)
inBlock = b"\x00" * 8 + msghash + salt
hash = self.hash(inBlock)
PSlen=emLen - self.digestLen - slen - 1 - 1
DB = (PSlen * b"\x00") + b"\x01" + salt
rlen = emLen - len(hash) - 1
dbMask = self.mgf1(hash, rlen)
maskedDB = bytearray()
for i in range(0, len(dbMask)):
maskedDB.append(dbMask[i] ^ DB[i])
maskedDB[0]=maskedDB[0]&0x7F
EM = maskedDB + hash + b"\xbc"
tosign=self.os2ip(EM)
#EM=hexlify(EM).decode('utf-8')
#tosign = int(EM,16)
return self.sign(tosign,D,N,emBits)
#6B1EAA2042A5C8DA8B1B4A8320111A70A0CBA65233D1C6E418EF8156E82A8F96BD843F047FF25AB9702A6582C8387298753E628F23448B4580E09CBD2A483C623B888F47C4BD2C5EFF09013C6DFF67DB59BAB3037F0BEE05D5660264D28CC6251631FE75CE106D931A04FA032FEA31259715CE0FAB1AE0E2F8130807AF4019A61B9C060ECE59104F22156FEE8108F17DC80D7C2F8397AFB9780994F7C5A0652F93D1B48010B0B248AB9711235787D797FBA4D10A29BCF09628585D405640A866B15EE9D7526A2703E72A19811EF447F6E5C43F915B3808EBC79EA4BCF78903DBDE32E47E239CFB5F2B5986D0CBBFBE6BACDC29B2ADE006D23D0B90775B1AE4DD
def ceil_div(self, a, b):
(q, r) = divmod(a, b)
if r:
return q + 1
else:
return q
def pss_verify(self, e, N, msghash, signature, emBits=1024, salt=None):
if salt == None:
slen = self.digestLen
else:
slen = len(salt)
sig = self.os2ip(signature)
EM = pow(sig, e, N)
#EM = unhexlify(hex(EM)[2:])
EM=self.i2osp(EM,emBits//8)
emLen = len(signature)
valBC = EM[-1]
if valBC != 0xbc:
print("[rsa_pss] : 0xbc check failed")
return False
hash = EM[emLen - self.digestLen - 1:-1]
maskedDB = EM[:emLen - self.digestLen - 1]
lmask=~(0xFF >> (8 * emLen + 1 - emBits))
if EM[0]&lmask:
print("[rsa_pss] : lmask check failed")
return False
dbMask = self.mgf1(hash, emLen - self.digestLen - 1)
DB = bytearray()
for i in range(0, len(dbMask)):
DB.append(dbMask[i] ^ maskedDB[i])
TS = bytearray()
TS.append(DB[0] & ~lmask)
TS.extend(DB[1:])
PS = (b"\x00" * (emLen - self.digestLen - slen - 2)) + b"\x01"
if TS[:len(PS)] != PS:
print(TS[:len(PS)])
print(PS)
print("[rsa_pss] : 0x01 check failed")
return False
if salt != None:
inBlock = b"\x00" * 8 + msghash + salt
mhash = self.hash(inBlock)
if mhash == hash:
return True
else:
return False
else:
salt=TS[-self.digestLen:]
inBlock = b"\x00" * 8 + msghash + salt
mhash = self.hash(inBlock)
if mhash == hash:
return True
else:
return False
return maskedDB
class hash():
def __init__(self, hashtype="SHA256"):
if hashtype == "SHA1":
self.hash = self.sha1
self.digestLen = 0x14
elif hashtype == "SHA256":
self.hash = self.sha256
self.digestLen = 0x20
elif hashtype == "MD5":
self.hash = self.md5
self.digestLen = 0x10
def sha1(self, msg):
return hashlib.sha1(msg).digest()
def sha256(self, msg):
return hashlib.sha256(msg).digest()
def md5(self, msg):
return hashlib.md5(msg).digest()

View file

@ -87,6 +87,17 @@ class qualcomm_firehose:
def cmd_reset(self): def cmd_reset(self):
data = "<?xml version=\"1.0\" ?><data><power value=\"reset\"/></data>" data = "<?xml version=\"1.0\" ?><data><power value=\"reset\"/></data>"
val=self.xmlsend(data) val=self.xmlsend(data)
try:
v=None
while(v!=b''):
v=self.cdc.read()
if v!=b'':
resp = self.xml.getlog(v)[0]
else:
break
print(resp)
except:
pass
if val[0]==True: if val[0]==True:
logger.info("Reset succeeded.") logger.info("Reset succeeded.")
return True return True
@ -404,9 +415,10 @@ class qualcomm_firehose:
v = b'-1' v = b'-1'
#try: #try:
if lvl!=1: if lvl!=1:
#self.cdc.timeout = 50 self.cdc.timeout = 50
info=[] info=[]
while v != b'': while v != b'':
try:
v = self.cdc.read() v = self.cdc.read()
if v==b'': if v==b'':
break break
@ -415,6 +427,8 @@ class qualcomm_firehose:
info.append(data[0]) info.append(data[0])
if info=='': if info=='':
break break
except:
pass
#if info==[]: #if info==[]:
# info=self.cmd_nop() # info=self.cmd_nop()
@ -452,7 +466,7 @@ class qualcomm_firehose:
if "MaxPayloadSizeFromTargetInBytes" in rsp[1]: if "MaxPayloadSizeFromTargetInBytes" in rsp[1]:
self.cfg.MaxPayloadSizeFromTargetInBytes=int(rsp[1]["MaxPayloadSizeFromTargetInBytes"]) self.cfg.MaxPayloadSizeFromTargetInBytes=int(rsp[1]["MaxPayloadSizeFromTargetInBytes"])
else: else:
self.cfg.MaxPayloadSizeFromTargetInBytes=8192 self.cfg.MaxPayloadSizeFromTargetInBytes=self.cfg.MaxXMLSizeInBytes
logging.warning("Couldn't detect MaxPayloadSizeFromTargetinBytes") logging.warning("Couldn't detect MaxPayloadSizeFromTargetinBytes")
if "TargetName" in rsp[1]: if "TargetName" in rsp[1]:
self.cfg.TargetName=rsp[1]["TargetName"] self.cfg.TargetName=rsp[1]["TargetName"]

View file

@ -127,11 +127,11 @@ class usb_class():
def close(self): def close(self):
if (self.connected==True): if (self.connected==True):
usb.util.dispose_resources(self.device) usb.util.dispose_resources(self.device)
if self.device.is_kernel_driver_active(self.interface): try:
try: if self.device.is_kernel_driver_active(self.interface):
self.device.attach_kernel_driver(self.interface) self.device.attach_kernel_driver(self.interface)
except: except:
pass pass
def write(self,command,pktsize=64): def write(self,command,pktsize=64):
pos=0 pos=0
@ -160,9 +160,9 @@ class usb_class():
try: try:
tmp=self.device.read(self.EP_IN, length,timeout) tmp=self.device.read(self.EP_IN, length,timeout)
except usb.core.USBError as e: except usb.core.USBError as e:
if "timed out" in e.strerror: if b"timed out" in e.strerror:
#time.sleep(0.05) time.sleep(0.05)
#print("Waiting...") print("Waiting...")
return bytearray(tmp) return bytearray(tmp)
elif e.errno != None: elif e.errno != None:
print(repr(e), type(e), e.errno) print(repr(e), type(e), e.errno)

View file

@ -15,13 +15,20 @@
- Copy Drivers/50-android.rules to /etc/udev/rules.d - Copy Drivers/50-android.rules to /etc/udev/rules.d
- sudo apt install adb - sudo apt install adb
- sudo apt install fastboot - sudo apt install fastboot
- sudo sudo apt install liblzma-dev
Linux/Windows: Linux/Windows:
- "python -m pip install -r requirements.txt" - "python -m pip install -r requirements.txt"
Windows: Windows:
- Use Filter Installer to install libusb filter driver - Boot device into 9008 mode, install Qualcomm_Diag_QD_Loader_2016_driver.exe from Drivers\Windows,
on Qualcomm 9008 port otherwise we won't detect the device then install libusb-win32-devel-filter-1.2.6.0.exe.
Run Filter Wizard, "Install a device filter", select "Qualcomm HS-USB QDLoader 9008"
and press "Install" to install libusb filter driver otherwise we won't detect the device.
Then use the edl tool.
or
- Use Zadig 2.4 or higher, list all devices, select QUSB_BULK device and replace
driver with libusb 1.2.6 one (will replace original driver)
## Run EDL (examples) ## Run EDL (examples)

View file

@ -1,3 +1,4 @@
pyusb pyusb
pyserial pyserial
docopt docopt
pylzma