python-validity/db97.py
Viktor Dragomiretskyy ef6ce24a26 A couple of improvements
- make TLS session persistent:
  once open, you can save TLS state with tls.save() and
  restore it next time by calling tls.load() instad of tls.open().
  State is saved in tls.dict file.
- a small standalone script added which can be used to keep an open
  file descriptor to the device. Without it tls.load() wont work.
- identify() function added
- added methods to associate blobs with fingers. Windows seems to be
  using these blobs to save certificates for the users to establish
  trust with potentially removable devices (it is sort of documented in
  winbio specs)
2019-05-21 22:41:54 +12:00

257 lines
11 KiB
Python

from usb97 import unhex
from util import assert_status
from struct import pack, unpack
from binascii import hexlify, unhexlify
from sid import *
class UserStorage():
def __init__(self, dbid, name):
self.dbid=dbid
self.name=name
self.users=[]
def __repr__(self):
return '<UserStorage: dbid=%04x name=%s users=%s>' % (self.dbid, repr(self.name), repr(self.users))
class User():
def __init__(self, dbid, identity):
self.dbid=dbid
self.identity=identity
self.fingers=[]
def __repr__(self):
return '<User: dbid=%04x identity=%s fingers=%s>' % (self.dbid, repr(self.identity), repr(self.fingers))
def subtype_to_string(s):
if s < 0xf5 or s > 0xfe:
return 'Unknown'
return 'WINBIO_FINGER_UNSPECIFIED_POS_%02d' % (s - 0xf5 + 1)
def parse_user_storage(rsp):
assert_status(rsp[:2])
rsp=rsp[2:]
hdr, rsp = rsp[:8], rsp[8:]
recid, usercnt, namesz, unknwn = unpack('<HHHH', hdr)
usrtab, rsp = rsp[:4*usercnt], rsp[4*usercnt:]
name, rsp = rsp[:namesz], rsp[namesz:]
if len(rsp) > 0:
raise Exception('Junk at the end of the storage info response: %s' % unhexlify(rsp).decode())
storage=UserStorage(recid, name)
while len(usrtab) > 0:
rec, usrtab = usrtab[:4], usrtab[4:]
urid, valsz = unpack('<HH', rec)
storage.users += [ { 'dbid': urid, 'valueSize': valsz } ]
return storage
def parse_identity(b):
t, b = b[:4], b[4:]
t, = unpack('<L', t)
if t == 3:
l, b = b[:4], b[4:]
l, = unpack('<L', l)
return sid_from_bytes(b[:l])
raise Exception('Don''t know how to handle identity type %d' % t)
def parse_user(rsp):
assert_status(rsp[:2])
rsp=rsp[2:]
hdr, rsp = rsp[:8], rsp[8:]
recid, fingercnt, unknwn, identitysz = unpack('<HHHH', hdr)
fingertab, rsp = rsp[:8*fingercnt], rsp[8*fingercnt:]
identity, rsp = rsp[:identitysz], rsp[identitysz:]
if len(rsp) > 0:
raise Exception('Junk at the end of the user info response: %s' % unhexlify(rsp).decode())
identity = parse_identity(identity)
user=User(recid, identity)
while len(fingertab) > 0:
rec, fingertab = fingertab[:8], fingertab[8:]
frid, subtype, stgid, valsz = unpack('<HHHH', rec)
user.fingers += [ { 'dbid': frid, 'subtype': subtype, 'storage': stgid, 'valueSize': valsz } ]
return user
def identity_to_bytes(identity):
if isinstance(identity, SidIdentity):
b=identity.to_bytes()
b = pack('<LL', 3, len(b)) + b
# May not be neccessary, but windows union has a minimum size of 0x4c bytes
# and search by identity treats same SIDs with different sizes as different keys
while len(b) < 0x4c:
b += b'\0'
return b
else:
raise Exception('Don''t know how to handle identity %s' % repr(identity))
wtf_hardcoded=unhex('''
0602000001648445315dac4d9c91edfb3c327ec18aee87a1fd5e0993fe74b56ee9a70f0eb4d65bfc877060728ad26e695be98899ef76b85
2162cc5e6ccded51e5a26e7e345c4a8e83aafa2e10fa4cc78f0f34575b0565089b4c78ce1ac7fe3744f4721aa73209fa4a8699e7161bfd7
ee58dee33f1af15ba3b9ce9a469c5c82eda2b2a2f01658ee14118304e865bc2ad415bec9f06757bb3f2263bd0349a3c089380b72a30edf8
db01db75586c60ddc9e7bedc8cf9006fcc6825b0ec9dc8782b95f53325bfb6a6945644f2803a4c7fd35574d8b7b089102fb072dfa883049
f9484a789ff763a029f78adfceac92db195573c65986e38aec2135b5da3c0797939154135be5227515602c0148dbcb8c8a3760dad71f877
471244cf1053a3cc4bc73c437589a1a47e08c260ea40d3259138e754a556a23e09bf926ad90ff36096c27c460ebdd28f20f31e9f42a1925
26d79b94e7f2c36b66db69946d93edf8fbd516e3ae59a0b81400c39f6bc1feb209924bc04703fd36ab53dd5658b2dd91633823917c37e94
4e740409c5fd66974ed42b73d5d7db519c7037b1ed32e471750da95299463cb070c80cea4a03bf20351716ee14423f84670c53ef7b998c1
79b84ae91cff6ca6b33a372e9947eebc873c6a98c6bcda68e5278b6f799a468bd1a33ae07bf717fbfc20f4d019f0fb25a5d6cb75a97fbd9
b8490e6204d272d93f62ad393307cad3e24d292040e61fce6f1104c42dc4260de15f42971bbab18064b5dd6ceb12a32d9d25614d9326048
052bc178b5a0aed3f350cbbec021c4b1ac6ed383f7f4eca0e475cf56caefc3edd3cde6526401936cc095b2df8e0008b2d9bd78c82af0a1e
c0a71f047dd06b4ce6e91886607c55536f0d8712eae4ccb890008ccc27f2d6f6cb9b9bb88ec205019ec722160ffb1c65ca3177cff09c4bd
ae8c40be5bf91488a600ed199d0aad40510afe580802c56c0241a8b2a836800e8a0d07ae37176934252f92fc0e501aee29e6c3567e37b3e
1992ff00b561fd1b3ae37caecc3a1fea7720b0806d3b39cf8a3fb46087b43775b9e473e65cb18af1c9d87a5140a5edf8b7843524910d631
79c436b6a806d100eb066db889768f7da603631d75a6d79e3d0eb1fe9ba55401b828d4a367f695f3922b982be10fdb1f139a0b1438abc91
1db5da00ed464d80e3086d368dd131985a21610cd485740599afa772b5dc44d1906ea4c13b8e10b8db896fb81b706038a3328cd07aeb049
e6ec92ecd30044c54eb171e9cf7a0034cd24614616048491f576826af5ca6afe4bc01f86dc01c2ea25efb32bc9b1c8d56cc7d4d86adf8f1
f3bb6a649f98bae686ef93524721c618fad85a8125a8ac7d72bfeb7de9f590e8f43535e59259a7a1d13a12a1af91c8ba0dd45ddad02e5b8
6224fc00dcbc143b0e1d2da638d6798fcc340bda8f1e3abf21cadb2161c4c4be7001371dc56f0d112b42b113288effea6cab8c4b610f730
37643de21f433774d9fb28cfd213a62dcafab8b662b57e77239dc2fb69f026fcb773f09129c8db04b82dce68734516ffcb938d43deeeeda
a02cc08d7d06f34ec55ba4b224d3270265001df114b946d4238d51cfbfb84cf715b745ec918259c1165ee12ab82b4373eaa2b0c7e68da3e
b869ff033d5953800012bb006d1cf17dcbbe2fb2e129614351c000cab8ccc7c389df33dbe504a0c1df37868b839ef84a6c04072d69fc866
33055b2569c9c04f1994758987fc7063b97853c727809185a25cd69bfebf6a4367760fd21e7942b997bb2b7098dbec715769d13413c1c93
185409e54a2c9a97daf63114c71348f19549fb3a8551afbbb7476842f7045a0ec8e6d97fb4bb6de76090687ab4e9991a31e7625ba7088a3
263797be4787db02573b72100b3ba117e8897b939548715bcfa11d98861b60fa2a4759630b47cbbf329fbc675c4cfc2f963cc9ba72cbe67
13aff1851869917ba0b761f6532d1dee95331c4c621bb0513e993f7e6dda6da1e3076829b24093c8338c125aa2f5a0730f9e5631f7a9594
c13025fde9bb4f2431540a1c743c7fc55fc8909d85847fe71af8a68d2981b0eee84a308e0160fc4226aa679ca9b35d0aacae8240aabd5b2
17c5e66ac4795596f8742acb9869c3f52a96b1d946ff72f403ba491c1a0e4be778efab9d61a250562d705c2b459fc3a420143daf9355226
4bb8c7caa64143697961fab1b6af9e852d43a94736e1e75e4b9a7c2bee73598360c7e4c648e47ba9400dbe22b3eea6d9ab87fe677b36242
57f81d26b87536a642a7840bdccd8fce05c23891a4fa4ce2156fba2856313653207bf225b8114005432824d16a93473deb81721c77196af
d80cf723e8cdea81aaed19f55a09a8986f2950c49f4eebfb142d5e4815632d93ccb0106c156b47efca25dc3d7f31eb9b2dee0d2d83a51cb
e5d1270222920f2c87e5acc884c11b0df7a7e1bb49b06fa4f936964d3f2782fa46eec3e430815f82ab0fb5eb82377277f8fe3ae5b33732d
7d76ba3de284c66fba372f04f9ad6a39fa669a00583313d51c8e6b1eb960bf1e82ae8e0504b2d3035b371d5ecf638c1cc0c1dd67eff286f
c9ee0950da53f339a85fbbcbacf61fcdadee8cdba0950d23ec71e2239ae7ab5be1bf56364897af990428822d23deed81fbbe622420234de
d92b8fc3073bfa0a9677cd9e24b9f81278c50584bdc880fa2cd5c5c4b03bd78be872a656f3ef74b30d4e66f1ff091a343f75f096e64ab25
b64d426862ed3f21dc54c6b6a8cb90b065c7b33b03e97acde7d58a2fa35664ec03ef1f82a30441a021ec008bee9444bbe50ec8ff457d32d
06a63b9dc84fd82fa110a30dd849bf9f8cb4d3599b9aa09a76ad1b7c52bb972e5b16670670994ef7471f2d738ba9430664b7a1c259745ba
8b2b9d98e695bad0b59e1fd989475a61570b2b677734c3f1887f9555b760811e20caba91eff59e9fb328561e12749c32f603bd50da5c449
7393ccd560e88f367a664c5f2f9fe73107e2664a07a0f015877ce085f469dd24be1a20ce9544692e23094650a991e5bd7ab7d3b575d5ef7
1549fa442fe75a79a6c9f598eb1396e9332878c73f8af9e8f9f0cc11a4b42245acbb421646d6181c8f7f602298042a37d8b57520a1d68d6
e3d473d5f1897e05db677c7ff6e260048255fdbe35fae42c791ea57ac0f2334e6ab5c650f59f3b27b1af30347314510a29fe19e20ac6e8e
eb1e84deb687d2dc3d5cf4b03dbe35d165e
''')
class DbRecord():
def __init__(self):
self.dbid = 0
self.type = 0
self.storage = 0
self.value = None
self.children = None
def __repr__(self):
return '<DbRecord: dbid=%d type=%d storage=%d value=%s children=%s>' % (
self.dbid,
self.type,
self.storage,
repr(self.value),
repr(self.children)
)
class Db():
def __init__(self, tls):
self.tls = tls
def get_user_storage(self, dbid=0, name=''):
name=name.encode()
if len(name) > 0:
name += b'\0'
return parse_user_storage(self.tls.app(pack('<BHH', 0x4b, dbid, len(name)) + name))
def get_user(self, dbid):
return parse_user(self.tls.app(pack('<BHHH', 0x4a, dbid, 0, 0)))
def lookup_user(self, identity):
stg = self.get_user_storage(name='StgWindsor')
data = identity_to_bytes(identity)
rsp = self.tls.app(pack('<BHHH', 0x4a, 0, stg.dbid, len(data)) + data)
rc, = unpack('<H', rsp[:2])
if rc == 0x04b3:
return None
else:
return parse_user(rsp)
def get_record_value(self, dbid):
rsp = self.tls.app(pack('<BH', 0x49, dbid))
assert_status(rsp)
rec = DbRecord()
rec.dbid, rec.type, rec.storage, sz = unpack('<xxHHHHxx', rsp[:12])
rec.value = rsp[12:12+sz]
return rec
def get_record_children(self, dbid):
rsp = self.tls.app(pack('<BH', 0x46, dbid))
assert_status(rsp)
rec = DbRecord()
rec.dbid, rec.type, rec.storage, sz, cnt = unpack('<xxHHHHHxx', rsp[:14])
rsp = rsp[14:]
rec.children=[]
for i in range(0, cnt):
dbid, typ = unpack('<HH', rsp[i:i+4])
rec.children += [{ 'dbid': dbid, 'type': typ }]
return rec
def del_record(self, dbid):
assert_status(self.tls.app(pack('<BH', 0x48, dbid)))
def new_record(self, parent, typ, storage, data):
assert_status(self.tls.app(b'\x45'))
assert_status(self.tls.app(wtf_hardcoded))
rsp = self.tls.app(pack('<BHHHH', 0x47, parent, typ, storage, len(data)) + data)
assert_status(rsp)
recid, = unpack('<H', rsp[2:])
self.flush_changes()
return recid
def new_user(self, identity):
data = identity_to_bytes(identity)
stg = self.get_user_storage(name='StgWindsor')
rec = self.new_record(stg.dbid, 5, stg.dbid, data)
return rec
def new_finger(self, userid, template):
stg = self.get_user_storage(name='StgWindsor')
# We ask to create an object of type 0xb,
# but because of the magical `wtf_hardcoded` in the new_record() it ends up being 0x6
rec = self.new_record(userid, 0xb, stg.dbid, template)
return rec
def new_data(self, parent, data):
stg = self.get_user_storage(name='StgWindsor')
data = pack('<HH', 1, len(data)) + data
rec = self.new_record(parent, 0x8, stg.dbid, data)
return rec
def flush_changes(self):
assert_status(self.tls.app(b'\x1a'))
def dump_all(self):
stg = self.get_user_storage(name='StgWindsor')
usrs = [self.get_user(u['dbid']) for u in stg.users]
for u in usrs:
print('%2d: User %s with %d fingers:' % (u.dbid, repr(u.identity), len(u.fingers)))
for f in u.fingers:
print(' %2d: %02x (%s)' % (f['dbid'], f['subtype'], subtype_to_string(f['subtype'])))