# Reading/Writing to the bulk in/out seems to have this kind of payload structure:
# 1) 8 bytes hard-coded prefix depending on out vs in (see below)
# 2) 2 bytes which are a kind of "check bytes" for the payload
# 3) hardcoded "00 00 00"
# 4) 2 bytes that seem to be some kind of type/subtype for different processes/events
# 5) then it seems to vary a bit more with additional payload data depending on different type of events; either some kind of payload directly or more "type" kind of stuff either before or after the payload
# TODO: Probably better/easier/nicer to move to the struct package for building payloads, but for now we will do it all super-hard-coded with bytes...
# Bytes prefix for every payload sent to and from device
WRITE_PREFIX=b'EGIS\x00\x00\x00\x01'
READ_PREFIX=b'SIGE\x00\x00\x00\x01'
# Derive the 2 "check bytes" for write payloads
# 32-bit big-endian sum of all 16-bit words (including check bytes) MOD 0xFFFF should be 0
# Standard method to make it easier to read from USB_BULK_IN
defread(length=4096,timeout=5000):
returndev.read(USB_BULK_IN,length,timeout)
# Standard method to make it easier to read from USB_INTERRUPT_IN (wait for response from fingerprint on physical device)
defwait_for_finger(length=64,timeout=10000):# 10s default timeout to put finger on sensor
returndev.read(USB_INTERRUPT_IN,length,timeout)
### Setup ###
# required every time before the device is used; the Windows driver seems to work that the driver is always running in the background so this setup is only done once on Windows startup
# Put the identifier for each fingerprint in an array in case it is helpful later? We can also use len(fingers_enrolled) to see how many fingerprints are enrolled
fingers_enrolled=[]
defsetup():
# Reset the device in case it was not closed out properly last time
dev.reset()
# set the active configuration. With no arguments, the first
# configuration will be the active one
dev.set_configuration(1)
# get an endpoint instance
cfg=dev.get_active_configuration()
intf=cfg[(0,0)]
## Initialization sequence (attempting to match from traces) ##
# this response seems to have slightly different payload if there are fingers regd already vs not? not sure logic, but for now we can instead use the next sequence instead for this detection?
print(f"Read: {read().tobytes()}")
write(b'\x07\x50\x19\x04\x00\x00\x01\x40')
# This response seems to return a payload with an identifier/signature of all fingers registered, so we can use it to know if we are ready to verify or not and provide other info
# The payload portion of this packet (listed as #5 in description above) seems to be composed like this:
# - 32 bytes per finger
# - seem to be some kind of identifier/hash of the finger?
# - The 32 byte block sequences can come in different order when fingers are added/removed?
# Then each of finger identifiers already registered
forfinreversed(fingers_enrolled):#TODO not sure if reversed is required? but seems to be so in trace anyway
payload+=f
# then 00 40
payload+=b'\x00\x40'
returnpayload
### End Setup ###
# Different methods for performing different operations: info, enroll, verify, and wipe
### INFO ###
definfo():
print("Device information:")
print(dev)
print(f"Number of fingers enrolled: {len(fingers_enrolled)}")
### ENROLL ###
# Sensor read response checks during enrollment
# TODO: In the Windows driver they seem to be able to tell difference between Move Lower / Higher / Left / Right but could not find any difference in payload for each of these conditions (it was all the same payload pattern for these "not in center" conditions)
# also as a "hack" python has trouble if the byte is 0x0a because it interprets this as a linebreak (char 0a) and will sometimes not match this as "any character" (".") so will use a hack (.|\n) as the placeholder to pick up just in case the byte is 0x0a
PARTIAL_READ_SUCCESS_REGEX=re.compile(READ_PREFIX+b'(.|\n)(.|\n)\x00\x00\x00\x04(.|\n)(.|\n)\x90\x00')# Maybe we don't need to care about this one? also some bytes seem to increment/decrement based on which valid_read this is, we could use this if we did not want to keep track of read number in our code
# In the trace it seems like the Windows driver actually kicks off a read to the INTERRUPT IN in a background thread much earlier in the process and it always "stays on" in the background
# Then you can sort of control what will be done using different commands to the BULK OUT and read results using the BULK IN
# But here as a temporary test, we will instead just have a single thread where we kick off the read of the sensor (the INTERRUPT IN) only when we need it instead of this read always running/waiting in the background
# Setup to read a print
write(b'\x04\x50\x1a\x00\x00')
print(f"Read: {read().tobytes()}")
write(b'\x04\x50\x17\x01\x00')
print(f"Read: {read().tobytes()}")
print("Waiting for fingerprint on sensor. Please touch the power button...")
wait_for_finger()
print("Fingerprint detected!")
write(b'\x04\x50\x17\x02\x00')
print(f"Read: {read().tobytes()}")
# Next payload from existing registered fingerprints
write(fingers_enrolled_payload())
# this packet returns specific hard-coded bytes if this is a good new finger to add, or some kind of longer payload if it is not
raiseValueError('That fingerprint is already enrolled. Try a different finger.')
# TODO: if this result then it is considered a bad packet payload from fingers_enrolled_payload() (bad CRC etc?)?
#b'\xf5\x8c\x00\x00\x00\x02\x6f\xe1'
# TODO: skip this for now and see if it works without it? otherwise wonder if this might have something to do with associated the current current user to these enrollments?
## If first time a new finger is being enrolled for this device session, thenn some kind of client info is exchanged? not sure but will just copy what was in the trace for now...
#if len(fingers_enrolled) == 0:
# # TODO is this next payload hardcoded or same every time?
# payload = b'\x6b\x50\x57'
# payload += b'\x01\x00\x00\x00\x62\x20\x76\x6b'
# payload += b'\x30\x62\xb2\xc1\xc7\x18\x55\xeb'
# payload += b'\x3a\xf3\x90\x70\xf9\x7f\x8b\x8b'
# payload += b'\xda\x93\x10\xa8\x87\xc8\x75\xa0'
# payload += b'\xe9\x73\xbf\x70\x31\x8f\x04\xc5'
# payload += b'\x00\xbf\x04\x77\xe8\xd3\x09\x0f'
# payload += b'\x5f\xa3\x1a\x20\x23\x60\x43\x78'
# payload += b'\x39\xf5\x73\x49\xbd\x25\xe2\x7c'
# payload += b'\xe3\xa9\xc9\x07\x18\xb5\x62\xb1'
# payload += b'\xa5\x2b\x83\x97\x84\x17\x43\x4b'
# payload += b'\xd1\x1a\x47\x5b\x94\x82\xa7\x3e'
# payload += b'\x7d\x26\xc0\xab\x38\x57\x59\xb3'
# payload += b'\x0d\x86\x59\x00\xb3\x6d\xe6\x00'
# payload += b'\x00'
# write(payload)
# print(f"Read: {read().tobytes()}") # TODO this response is quite larger and seems to have a bit of Windows / device / cert info with it?
write(b'\x07\x50\x16\x01\x00\x00\x00\x20')
print(f"Read: {read().tobytes()}")# TODO this response has some kind of identifier in it? not sure what it is used for (is this the user ID?)
write(b'\x04\x50\x1a\x00\x00')
print(f"Read: {read().tobytes()}")
write(b'\x04\x50\x16\x02\x01')
print(f"Read: {read().tobytes()}")
read_prompt="Began registration of a new fingerprint. Please touch the sensor again..."
read_prompt="Finger was not centered on the sensor. Please try to move to the center and try again..."#TODO how does Windows driver tell difference between higher/lower/left/right?
# Build new enrolled fingerprint identifier payload
# first is a hardcoded string of bytes
payload=b'\x27\x50\x16\x03\x00\x00\x00\x20'
# Then the rest is actually the "identifier" for the fingerprint (which is sent back from device later) - how should this be generated?
# I think the driver is actually creating and assigning these here? Not sure how it is generated, maybe start with just generating a new 32 byte token?
new_finger_id=secrets.token_bytes(32)
payload+=new_finger_id
write(payload)
print(f"Read: {read().tobytes()}")
# TODO: In theory this should be "per user" e.g. per os.getlogin() and then that these IDs per user are saved somewhere like a file or database?
# "Verified" seems to be a prefix of 2 bytes (?), then "00 00 00 00 42", then 32 bytes of "something?" (is this the user ID?), then a 32 byte id of the fingerprint which was verified, then "90 00"
# So I assume you can figure out which user is associated with the verification based on which users has this fingerprint ID
VERIFIED_REGEX=re.compile(READ_PREFIX+b'(.|\n)(.|\n)\x00\x00\x00\x42(.|\n){32}((.|\n){32})\x90\x00')# TODO: think this is still not working exactly right to capture the ID?
# If "not verified," instead of matching the above pattern it seems like it is usually a hardcoded byte string on the same read?
# setup to read a print - in the trace this seems to be done after the read is kicked off on INTERRUPT IN but is it ok to do single-threaded like this?
write(b'\x04\x50\x17\x01\x01')
print(f"Read: {read().tobytes()}")
print("Waiting for fingerprint on sensor. Please touch the power button...")
wait_for_finger()
print("Fingerprint detected!")
# post-processing of some kind?
write(b'\x04\x50\x17\x02\x00')
print(f"Read: {read().tobytes()}")
# Next payload from existing registered fingerprints
write(fingers_enrolled_payload())
verify_finger_payload=read()
print(f"Read: {verify_finger_payload.tobytes()}")
result=False
mtch=VERIFIED_REGEX.match(verify_finger_payload)
ifnotmtch:
result=False
print('Your fingerprint could not be recognized. Please try a different finger.')
else:
result=True
print("Matched fingerprint! TODO: capture ID and match vs user")
## TODO: This capture / check may not be needed but also seems to be an issue with the regex/logic -- comment out for now...
#finger_id = mtch.group(4) # needs to be 4 due to workaround with (.|\n), so the finger ID is the 5th capture group we can get from the match
#if finger_id in fingers_enrolled:
# print(f"Found and matched fingerprint {finger_id} !")
#else:
# raise ValueError('Fingerprint seemed to be valid but could not be found in existing list of enrolled prints; what happened???')
write(b'\x04\x50\x1a\x00\x00')
print(f"Read: {read().tobytes()}")
write(b'\x04\x50\x17\x01\x01')
print(f"Read: {read().tobytes()}")
write(b'\x04\x50\x04\x01\x00')
print(f"Read: {read().tobytes()}")
# Get a response from the sensor a second time - why?
print("Getting second response from sensor...")
second_finger_response=wait_for_finger()
# TODO: does this need to be checked for valid/invalid ?