Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a function to verify QR signature #22

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pyaadhaar.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ Classifier: Programming Language :: Python :: 3.8
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: opencv-python
Requires-Dist: Pillow
Requires-Dist: pylibjpeg
Requires-Dist: pylibjpeg-openjpeg
Requires-Dist: python-dateutil
Requires-Dist: pytz
Requires-Dist: pyzbar
Requires-Dist: six
Requires-Dist: toml
Requires-Dist: pycryptodome

## PyAadhaar Library

Expand Down
1 change: 1 addition & 0 deletions pyaadhaar.egg-info/requires.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pytz
pyzbar
six
toml
pycryptodome
207 changes: 125 additions & 82 deletions pyaadhaar/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
import xml.etree.ElementTree as ET
from io import BytesIO
import base64
import zipfile
from typing import Union
from . import utils
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Util.number import long_to_bytes

class AadhaarSecureQr:
# This is the class for Aadhaar Secure Qr code.. In this version of code the data is in encrypted format
Expand All @@ -17,19 +20,26 @@ class AadhaarSecureQr:

def __init__(self, base10encodedstring:str) -> None:
self.base10encodedstring = base10encodedstring
self.details = ["version","email_mobile_status","referenceid", "name", "dob", "gender", "careof", "district", "landmark",
self.details = ["version", "email_mobile_status", "reference_id", "name", "dob", "gender", "careof", "district", "landmark",
"house", "location", "pincode", "postoffice", "state", "street", "subdistrict", "vtc", "last_4_digits_mobile_no"]
self.delimeter = [-1]
self.delimiter = [-1]
self.data = {}
self._convert_base10encoded_to_decompressed_array()
self._check_aadhaar_version()
self._create_delimeter()
self._create_delimiter()
self._extract_info_from_decompressed_array()

# Converts base10encoded string to a decompressed array
def _convert_base10encoded_to_decompressed_array(self) -> None:
bytes_array = self.base10encodedstring.to_bytes(5000, 'big').lstrip(b'\x00')
self.decompressed_array = zlib.decompress(bytes_array, 16+zlib.MAX_WBITS)
# print(f"Bytes array (trimmed leading zeros): {bytes_array[:100]}") # Debugging: print the first 100 bytes
try:
self.decompressed_array = zlib.decompress(bytes_array, 16 + zlib.MAX_WBITS)
# print(f"Decompressed array: {self.decompressed_array[:100]}") # Debugging: print the first 100 bytes of decompressed array
except Exception as e:
print(f"Decompression error: {e}")
return


# This function will check for the new 2022 version-2 Aadhaar QRs
# If not found it will remove the "version" key from self.details, Defaulting to normal Secure QRs
Expand All @@ -39,24 +49,40 @@ def _check_aadhaar_version(self) -> None:
self.details.pop() # Removing "Last_4_digits_of_mobile_no"

# Creates the delimeter which is used to extract the information from the decompressed array
def _create_delimeter(self) -> None:
def _create_delimiter(self) -> None:
for i in range(len(self.decompressed_array)):
if self.decompressed_array[i] == 255:
self.delimeter.append(i)
self.delimiter.append(i)

print(f"Delimiters: {self.delimiter}")


# Extracts the information from the decompressed array
def _extract_info_from_decompressed_array(self) -> None:
for i in range(len(self.details)):
self.data[self.details[i]] = self.decompressed_array[self.delimeter[i] + 1:self.delimeter[i+1]].decode("ISO-8859-1")
self.data['aadhaar_last_4_digit'] = self.data['referenceid'][:4]
self.data['aadhaar_last_digit'] = self.data['referenceid'][3]
aadhaar_prop_name = self.details[i]
start_idx = self.delimiter[i] + 1
end_idx = self.delimiter[i + 1]
self.data[aadhaar_prop_name] = self.decompressed_array[start_idx : end_idx].decode("ISO-8859-1")

# print(f"Extracted data before verification: {self.data}")

reference_id = self.data.get('reference_id', '')
if len(reference_id) >= 4:
self.data['aadhaar_last_4_digit'] = reference_id[:4]
self.data['aadhaar_last_digit'] = reference_id[3]
else:
print(f"Reference ID is too short: {reference_id}")
print(f"Extracted data: {self.data}")

# Default values to 'email' and 'mobile
self.data['email'] = False
self.data['mobile'] = False
# Updating the fields of 'email' and 'mobile'
if int(self.data['email_mobile_status']) in {3, 1}:
email_mobile_status = int(self.data.get('email_mobile_status', -1))
if email_mobile_status in {3, 2}:
self.data['email'] = True
if int(self.data['email_mobile_status']) in {3, 2}:
if email_mobile_status in {3, 1}:
self.data['mobile'] = True

# Returns the extracted data in a dictionary format
Expand All @@ -71,6 +97,34 @@ def signature(self) -> bytes:
def signedData(self) -> bytes:
return self.decompressed_array[:len(self.decompressed_array)-256]

def verify_signature(self, cert_file_path:str = 'pyaadhaar/uidai_publickey.pem') -> bool:
"""
Verifies the signature of the Aadhaar Secure QR code.
From Secure QR documentation (User_manulal_QR_Code_15032019.pdf):
Validate (signature value and signed data value) by using public key with algorithm SHA256withRSA.
The default public key is "pyaadhaar/uidai_publickey.pem", which is extracted from the certificate
present in the offline KYC XML (uidai_certificate_from_xml.cer).
> openssl x509 -inform PEM -in uidai_certificate_from_xml.cer -pubkey -noout > uidai_publickey.pem

Args:
cert_file_path (str, optional): The path to the certificate file. Defaults to 'pyaadhaar/uidai_publickey.pem'.

Returns:
bool: True if the signature is valid, False otherwise.
"""
with open(cert_file_path, 'rb') as key_file:
public_key = RSA.import_key(key_file.read())

h = SHA256.new(self.signedData())
verifier = pkcs1_15.new(public_key)

try:
verifier.verify(h, self.signature())
return True
except (ValueError, TypeError) as e:
print(f"Signature verification failed: {e}")
return False

# Check whether mobile no is registered or not
def isMobileNoRegistered(self) -> bool:
return self.data['mobile']
Expand All @@ -79,87 +133,76 @@ def isMobileNoRegistered(self) -> bool:
def isEmailRegistered(self) -> bool:
return self.data['email']

# Return hash of the email id
# Return hash of the email id.
# Check the value of Email_mobile_present_bit_indicator_value:
# * If its 3 then first read mobile from index (Byte array length - 1 - 256) and
# then email from index (Byte array length - 1 - 256 - 32) in reverse order. Each value will be of fix size of 32 byte.
#
# * If Email_mobile_present_bit_indicator_value is 1 then only mobile is present.
#
# * If Email_mobile_present_bit_indicator_value is 2 then only email is present.
#
# * If Email_mobile_present_bit_indicator_value is 0 then no mobile or email present.
#
# Email and Mobile value will available in byte. Convert into Hexadecimal String
def sha256hashOfEMail(self) -> str:
tmp = ""
if int(self.data['email_mobile_status']) == 3:
tmp = self.decompressed_array[len(self.decompressed_array)-256-32-32:len(self.decompressed_array)-256-32].hex()
elif int(self.data['email_mobile_status']) == 1:
tmp = self.decompressed_array[len(self.decompressed_array)-256-32:len(self.decompressed_array)-256].hex()

if self.isEmailRegistered() and self.isMobileNoRegistered():
start_idx = len(self.decompressed_array)-256-32-32
end_idx = len(self.decompressed_array)-256-32
tmp = self.decompressed_array[start_idx : end_idx].hex()
elif self.isEmailRegistered() and not self.isMobileNoRegistered():
start_idx = len(self.decompressed_array)-256-32
end_idx = len(self.decompressed_array)-256
tmp = self.decompressed_array[start_idx : end_idx].hex()

return tmp

# Return hash of the mobile number
def sha256hashOfMobileNumber(self) -> str:
return (
self.decompressed_array[
len(self.decompressed_array)
- 256
- 32 : len(self.decompressed_array)
- 256
].hex()
if int(self.data['email_mobile_status']) in {3, 2}
else ""
)
tmp = ""

if self.isMobileNoRegistered():
start_idx = len(self.decompressed_array)-256-32
end_idx = len(self.decompressed_array)-256
tmp = self.decompressed_array[start_idx : end_idx].hex()

return tmp

# Check availability of image in the QR CODE
def isImage(self, buffer = 10) -> bool:
if int(self.data['email_mobile_status']) == 3:
return (
len(
self.decompressed_array[
self.delimeter[len(self.details)] + 1 :
]
)
>= 256 + 32 + 32 + buffer
)
elif int(self.data['email_mobile_status']) in {2, 1}:
return (
len(
self.decompressed_array[
self.delimeter[len(self.details)] + 1 :
]
)
>= 256 + 32 + buffer
)
elif int(self.data['email_mobile_status']) == 0:
return (
len(
self.decompressed_array[
self.delimeter[len(self.details)] + 1 :
]
)
>= 256 + buffer
)

num_aadhaar_props = len(self.details)
start_idx = self.delimiter[num_aadhaar_props] + 1
len_image_bytes = len(self.decompressed_array[start_idx :])

if self.isEmailRegistered() and self.isMobileNoRegistered():
return (len_image_bytes >= (256 + 32 + 32 + buffer))
elif self.isEmailRegistered() or self.isMobileNoRegistered():
return (len_image_bytes >= (256 + 32 + buffer))
elif not self.isEmailRegistered() and not self.isMobileNoRegistered():
return (len_image_bytes >= (256 + buffer))

# Return image stream
def image(self) -> Union[Image.Image,None]:
if int(self.data['email_mobile_status']) == 3:
return Image.open(
BytesIO(
self.decompressed_array[
self.delimeter[len(self.details)] + 1 :
]
)
)
elif int(self.data['email_mobile_status']) in {2, 1}:
return Image.open(
BytesIO(
self.decompressed_array[
self.delimeter[len(self.details)] + 1 :
]
)
)
elif int(self.data['email_mobile_status']) == 0:
return Image.open(
BytesIO(
self.decompressed_array[
self.delimeter[len(self.details)] + 1 :
]
)
)
def image(self, format:str = 'img') -> Union[Image.Image,None]:
image_byte_data = None
num_aadhaar_props = len(self.details)
start_idx = self.delimiter[num_aadhaar_props] + 1

if self.isEmailRegistered() and self.isMobileNoRegistered():
image_byte_data = self.decompressed_array[start_idx : ]
elif self.isEmailRegistered() or self.isMobileNoRegistered():
image_byte_data = self.decompressed_array[start_idx : ]
elif not self.isEmailRegistered() and not self.isMobileNoRegistered():
image_byte_data = self.decompressed_array[start_idx : ]
else:
return None

if 'bytedata' == format:
return image_byte_data
else:
return Image.open(BytesIO(image_byte_data))

# Save the image of the user
def saveimage(self, filepath:str) -> None:
image = self.image()
Expand All @@ -173,7 +216,7 @@ def verifyEmail(self, emailid:str) -> bool:
generated_sha_mail = utils.SHAGenerator(emailid, self.data['aadhaar_last_digit'])
return generated_sha_mail == self.sha256hashOfEMail()

# Verify the mobile no
# Verify the mobile no
def verifyMobileNumber(self, mobileno:str) -> bool:
if type(mobileno) != str:
raise TypeError("Mobile number should be string")
Expand Down Expand Up @@ -318,4 +361,4 @@ def verifyMobileNumber(self, mobileno:str) -> bool:
if generated_sha_mobile == self.sha256hashOfMobileNumber():
return True
else:
return False
return False
37 changes: 37 additions & 0 deletions pyaadhaar/uidai_certificate_from_xml.cer
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-----BEGIN CERTIFICATE-----
MIIHwjCCBqqgAwIBAgIEU5laMzANBgkqhkiG9w0BAQsFADCB/DELMAkGA1UEBhMCSU4xQTA/BgNV
BAoTOEd1amFyYXQgTmFybWFkYSBWYWxsZXkgRmVydGlsaXplcnMgYW5kIENoZW1pY2FscyBMaW1p
dGVkMR0wGwYDVQQLExRDZXJ0aWZ5aW5nIEF1dGhvcml0eTEPMA0GA1UEERMGMzgwMDU0MRAwDgYD
VQQIEwdHdWphcmF0MSYwJAYDVQQJEx1Cb2Rha2RldiwgUyBHIFJvYWQsIEFobWVkYWJhZDEcMBoG
A1UEMxMTMzAxLCBHTkZDIEluZm90b3dlcjEiMCAGA1UEAxMZKG4pQ29kZSBTb2x1dGlvbnMgQ0Eg
MjAxNDAeFw0yMTAyMjYxMTU0MjRaFw0yNDAyMjcwMDI3MTFaMIHdMQswCQYDVQQGEwJJTjExMC8G
A1UEChMoVU5JUVVFIElERU5USUZJQ0FUSU9OIEFVVEhPUklUWSBPRiBJTkRJQTEPMA0GA1UEERMG
MTEwMDAxMQ4wDAYDVQQIEwVEZWxoaTEbMBkGA1UECRMSQkVISU5EIEtBTEkgTUFORElSMSQwIgYD
VQQzExtBQURIQVIgSFEgQkFOR0xBIFNBSElCIFJPQUQxNzA1BgNVBAMTLkRTIFVOSVFVRSBJREVO
VElGSUNBVElPTiBBVVRIT1JJVFkgT0YgSU5ESUEgMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQCiciwOXy3lunB+2T8DbsKx8LlVkyOQ+swPC8vyDIChXAiLSIaGa3LrJasL9Vov4Gtp
7b1cyDt0x3CdshQebAfGi834WdPa9/P87SQdByBV3BVIhHS0XCyYL6lUqlKqb/+ySBhhxlCF2Etk
FY6fQ9nzXKabSM6TAFIhAqTK4JO//UdLCNMtHQQG9of35VvSJqI4S/WKQcOEw5dPHHxRFYGckm3j
rfPsu5kExIbx9dUwOXe+pjWENnMptcFor9yVEhcx9/SNQ6988x9pseO755Sdx6ixDAvd66ur3r6g
dqHPgWat8GqKQd7fFDv/g129K9W7C2HSRywjSm1EEbybU2CVAgMBAAGjggNnMIIDYzAOBgNVHQ8B
Af8EBAMCBsAwKgYDVR0lBCMwIQYIKwYBBQUHAwQGCisGAQQBgjcKAwwGCSqGSIb3LwEBBTCCAQIG
A1UdIASB+jCB9zCBhgYGYIJkZAICMHwwegYIKwYBBQUHAgIwbgxsQ2xhc3MgMiBjZXJ0aWZpY2F0
ZXMgYXJlIHVzZWQgZm9yIGZvcm0gc2lnbmluZywgZm9ybSBhdXRoZW50aWNhdGlvbiBhbmQgc2ln
bmluZyBvdGhlciBsb3cgcmlzayB0cmFuc2FjdGlvbnMuMGwGBmCCZGQKATBiMGAGCCsGAQUFBwIC
MFQMUlRoaXMgY2VydGlmaWNhdGUgcHJvdmlkZXMgaGlnaGVyIGxldmVsIG9mIGFzc3VyYW5jZSBm
b3IgZG9jdW1lbnQgc2lnbmluZyBmdW5jdGlvbi4wDAYDVR0TAQH/BAIwADAjBgNVHREEHDAagRhy
YWh1bC5rdW1hckB1aWRhaS5uZXQuaW4wggFuBgNVHR8EggFlMIIBYTCCAR6gggEaoIIBFqSCARIw
ggEOMQswCQYDVQQGEwJJTjFBMD8GA1UEChM4R3VqYXJhdCBOYXJtYWRhIFZhbGxleSBGZXJ0aWxp
emVycyBhbmQgQ2hlbWljYWxzIExpbWl0ZWQxHTAbBgNVBAsTFENlcnRpZnlpbmcgQXV0aG9yaXR5
MQ8wDQYDVQQREwYzODAwNTQxEDAOBgNVBAgTB0d1amFyYXQxJjAkBgNVBAkTHUJvZGFrZGV2LCBT
IEcgUm9hZCwgQWhtZWRhYmFkMRwwGgYDVQQzExMzMDEsIEdORkMgSW5mb3Rvd2VyMSIwIAYDVQQD
ExkobilDb2RlIFNvbHV0aW9ucyBDQSAyMDE0MRAwDgYDVQQDEwdDUkw1Njk0MD2gO6A5hjdodHRw
czovL3d3dy5uY29kZXNvbHV0aW9ucy5jb20vcmVwb3NpdG9yeS9uY29kZWNhMTQuY3JsMCsGA1Ud
EAQkMCKADzIwMjEwMjI2MTE1NDI0WoEPMjAyNDAyMjcwMDI3MTFaMBMGA1UdIwQMMAqACE0HvvGe
nfu9MB0GA1UdDgQWBBTpS5Cfqf2zdwqjupLAqMwk/bqX9DAZBgkqhkiG9n0HQQAEDDAKGwRWOC4x
AwIDKDANBgkqhkiG9w0BAQsFAAOCAQEAbTlOC4sonzb44+u5+VZ3wGz3OFg0uJGsufbBu5efh7kO
2DlYnx7okdEfayQQs6AUzDvsH1yBSBjsaZo3fwBgQUIMaNKdKSrRI0eOTDqilizldHqj113f4eUz
U2j4okcNSF7TxQWMjxwyM86QsQ6vxZK7arhBhVjwp443+pxfSIdFUu428K6yH4JBGhZSzWuqD6GN
hOhDzS+sS23MkwHFq0GX4erhVfN/W7XLeSjzF4zmjg+O77vTySCNe2VRYDrfFS8EAOcO4q7szc7+
6xdg8RlgzoZHoRG/GqUp9inpJUn7OIzhHi2e8MllaMdtXo0nbr150tMe8ZSvY2fMiTCY1w==
-----END CERTIFICATE-----
9 changes: 9 additions & 0 deletions pyaadhaar/uidai_publickey.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAonIsDl8t5bpwftk/A27C
sfC5VZMjkPrMDwvL8gyAoVwIi0iGhmty6yWrC/VaL+Brae29XMg7dMdwnbIUHmwH
xovN+FnT2vfz/O0kHQcgVdwVSIR0tFwsmC+pVKpSqm//skgYYcZQhdhLZBWOn0PZ
81ymm0jOkwBSIQKkyuCTv/1HSwjTLR0EBvaH9+Vb0iaiOEv1ikHDhMOXTxx8URWB
nJJt463z7LuZBMSG8fXVMDl3vqY1hDZzKbXBaK/clRIXMff0jUOvfPMfabHju+eU
nceosQwL3eurq96+oHahz4FmrfBqikHe3xQ7/4NdvSvVuwth0kcsI0ptRBG8m1Ng
lQIDAQAB
-----END PUBLIC KEY-----
4 changes: 2 additions & 2 deletions pyaadhaar/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def SHAGenerator(string, n):

tmp_sha = str(string)
if int(n) == 0 or int(n) == 1:
return sha256(tmp_sha.encode()).hexdigest()
return sha256(tmp_sha.encode("ISO-8859-1")).hexdigest()
for i in range(int(n)):
tmp_sha = sha256(tmp_sha.encode()).hexdigest()
tmp_sha = sha256(tmp_sha.encode("ISO-8859-1")).hexdigest()
return tmp_sha


Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
'pytz',
'pyzbar',
'six',
'toml'
'toml',
'pycryptodome'
],

)