Skip to content

Commit

Permalink
Add support for generating FASC-N
Browse files Browse the repository at this point in the history
  • Loading branch information
christopherL91 committed Mar 12, 2024
1 parent 35cc678 commit 26e990b
Show file tree
Hide file tree
Showing 2 changed files with 309 additions and 0 deletions.
72 changes: 72 additions & 0 deletions yubikit/nist_piv/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from enum import IntEnum


class BCD(IntEnum):
bcd_zero = 0x01 # 00001 0x00 0
bcd_one = 0x10 # 10000 0x01 1
bcd_two = 0x08 # 01000 0x02 2
bcd_three = 0x19 # 11001 0x03 3
bcd_four = 0x04 # 00100 0x04 4
bcd_five = 0x15 # 10101 0x05 5
bcd_six = 0x0D # 01101 0x06 6
bcd_seven = 0x1C # 11100 0x07 7
bcd_eight = 0x02 # 00010 0x08 8
bcd_nine = 0x13 # 10011 0x09 9
bcd_colon = 0x0B # 01011 0x0a :
bcd_ss = 0x1A # 11010 0x0b ;
bcd_less = 0x07 # 00111 0x0c <
bcd_fs = 0x16 # 10110 0x0d =
bcd_grtr = 0x0E # 01110 0x0e >
bcd_es = 0x1F # 11111 0x0f ?

@classmethod
def translate(cls, b: int) -> int:
match b:
case 0x00:
return cls.bcd_zero
case 0x01:
return cls.bcd_one
case 0x02:
return cls.bcd_two
case 0x03:
return cls.bcd_three
case 0x04:
return cls.bcd_four
case 0x05:
return BCD.bcd_five
case 0x06:
return BCD.bcd_six
case 0x07:
return BCD.bcd_seven
case 0x08:
return BCD.bcd_eight
case 0x09:
return BCD.bcd_nine
case _:
raise Exception(f"unknown input '{b}'")

@classmethod
def reverse_translate(cls, b: int) -> int:
match b:
case BCD.bcd_zero:
return 0x00
case BCD.bcd_one:
return 0x01
case BCD.bcd_two:
return 0x02
case BCD.bcd_three:
return 0x03
case BCD.bcd_four:
return 0x04
case BCD.bcd_five:
return 0x05
case BCD.bcd_six:
return 0x06
case BCD.bcd_seven:
return 0x07
case BCD.bcd_eight:
return 0x08
case BCD.bcd_nine:
return 0x09
case _:
raise Exception(f"unknown input '{b}'")
237 changes: 237 additions & 0 deletions yubikit/nist_piv/fascn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
from __future__ import annotations

from yubikit.nist_piv import BCD

from abc import ABC
from bitarray import bitarray
from io import StringIO


class FASCNBuilder(ABC):
# Agency Code
ac: list[int] = [BCD.bcd_zero] * 4

# System Code
sc: list[int] = [BCD.bcd_zero] * 4

# Credential Number
cn: list[int] = [BCD.bcd_zero] * 6

# Credential Series
cs: int = BCD.bcd_zero

# Individual Credential Issue
ici: int = BCD.bcd_zero

# Person Identifier
pi: list[int] = [BCD.bcd_zero] * 10

# Organizational Category
oc: int = BCD.bcd_zero

# Organizational Identifier
oi: list[int] = [BCD.bcd_zero] * 4

# Person/Organization Association Category
poa: int = BCD.bcd_zero

def agency_code(self, ac: str):
n = list(map(int, ac))
self.ac = list(map(BCD.translate, n))
return self

def system_code(self, sc: str):
n = list(map(int, sc))
self.sc = list(map(BCD.translate, n))
return self

def credential_number(self, cn: str):
n = list(map(int, cn))
self.cn = list(map(BCD.translate, n))
return self

def credential_series(self, cs: str):
self.cs = BCD.translate(int(cs))
return self

def individual_credential_issue(self, ici: str):
self.ici = BCD.translate(int(ici))
return self

def person_identifier(self, pi: str):
n = list(map(int, pi))
self.pi = list(map(BCD.translate, n))
return self

def organizational_category(self, oc: str):
self.oc = BCD.translate(int(oc))
return self

def organization_association_category(self, poa: str):
self.poa = BCD.translate(int(poa))
return self

def build(self) -> FASCN:
return FASCN(self)


class FASCN(object):
def __init__(self, builder: FASCNBuilder):
self.ac = builder.ac
self.sc = builder.sc
self.cn = builder.cn
self.cs = builder.cs
self.ici = builder.ici
self.pi = builder.pi
self.oc = builder.oc
self.oi = builder.oi
self.poa = builder.poa

def encode(self) -> bytes:
b = bytearray()
b.append(BCD.bcd_ss.value)
b.extend(self.ac)
b.append(BCD.bcd_fs)
b.extend(self.sc)
b.append(BCD.bcd_fs)
b.extend(self.cn)
b.append(BCD.bcd_fs)
b.append(self.cs)
b.append(BCD.bcd_fs)
b.append(self.ici)
b.append(BCD.bcd_fs)
b.extend(self.pi)
b.append(self.oc)
b.extend(self.oi)
b.append(self.poa)
b.append(BCD.bcd_es)

bs = bitarray(200)
current_bit = 0
for item in b:
tmp = item << 3
mask = 0x80
for _ in range(0, 5):
if (tmp & mask) == mask:
bs[current_bit] = True
tmp = tmp << 1
current_bit += 1

bcd = bitarray(5)
for marker in range(0, 195, 5):
tmp = bs[marker : marker + 5]
bcd = bcd ^ tmp

# set error code
for append in range(195, 200):
if bcd[append - 195]:
bs[append] = True

out = bytearray()
current_bit = 0
for _ in range(0, 25):
current_byte = 0x00
for j in range(0, 8):
if bs[current_bit]:
current_byte = {
0: lambda x: x | 0x80,
1: lambda x: x | 0x40,
2: lambda x: x | 0x20,
3: lambda x: x | 0x10,
4: lambda x: x | 0x08,
5: lambda x: x | 0x04,
6: lambda x: x | 0x02,
7: lambda x: x | 0x01,
}[j](current_byte)
current_bit += 1
out.append(current_byte)
return out

@staticmethod
def decode(b: bytes) -> FASCN:
out = bytearray(b)
bs = bitarray(200)
current_bit = 0
for i in out:
temp_byte = i
mask = 0x80
for _ in range(0, 8):
if (temp_byte & mask) == mask:
bs[current_bit] = True
temp_byte = temp_byte << 1
current_bit += 1

out = bytearray()
for j in range(0, 200, 5):
current_byte = 0x00
tmp = bs[j : j + 5]
current_bit = 0
for k in range(0, 5):
if tmp[current_bit]:
current_byte = {
0: lambda x: x | 0x10,
1: lambda x: x | 0x08,
2: lambda x: x | 0x04,
3: lambda x: x | 0x02,
4: lambda x: x | 0x01,
}[k](current_byte)
current_bit += 1
out.append(current_byte)

fascn = FASCNBuilder().build() # default values
fascn.ac = out[1:5] # 4 bytes
fascn.sc = out[6:10] # 4 bytes
fascn.cn = out[11:17] # 6 bytes
fascn.cs = out[18] # 1 byte
fascn.ici = out[20] # 1 byte
fascn.pi = out[22:32] # 10 bytes
fascn.oc = out[32] # 1 byte
fascn.oi = out[33:37] # 5 bytes
fascn.poa = out[37] # 1 byte
return fascn

def __eq__(self, other: object) -> bool:
if isinstance(other, FASCN):
return self.encode() == other.encode()
return False

def __str__(self) -> str:
out = StringIO()
out.write(
"Agency Code: {}\n".format(
[str(x) for x in map(BCD.reverse_translate, self.ac)]
)
)
out.write(
"System Code: {}\n".format(
[str(x) for x in map(BCD.reverse_translate, self.sc)]
)
)
out.write(
"Credential Number: {}\n".format(
[str(x) for x in map(BCD.reverse_translate, self.cn)]
)
)
out.write("Credential Series: {}\n".format(BCD.reverse_translate(self.cs)))
out.write(
"Individual Credential Issue: {}\n".format(BCD.reverse_translate(self.ici))
)
out.write(
"Person Identifier: {}\n".format(
[str(x) for x in map(BCD.reverse_translate, self.pi)]
)
)
out.write(
"Organizational Category: {}\n".format(BCD.reverse_translate(self.oc))
)
out.write(
"Organizational Identifier: {}\n".format(
[str(x) for x in map(BCD.reverse_translate, self.oi)]
)
)
out.write(
"Person/Organization Association Category: {}\n".format(
BCD.reverse_translate(self.poa)
)
)
return out.getvalue()

0 comments on commit 26e990b

Please sign in to comment.