From: Agnibho Mondal Date: Thu, 12 Oct 2023 18:38:41 +0000 (+0530) Subject: Reimplmented prescription signing with cryptography module X-Git-Tag: v0.4~25 X-Git-Url: https://code.agnibho.com/repo?a=commitdiff_plain;h=d71be1423464095b1f38395ff3cde1d794e66f3b;p=medscript.git Reimplmented prescription signing with cryptography module --- diff --git a/README b/README index eaf0337..b879ea8 100644 --- a/README +++ b/README @@ -40,7 +40,6 @@ The required Python libraries are as follows: 4. Jinja2 5. lxml 6. Markdown -7. M2Crypto (optional) Usage ----- @@ -114,6 +113,16 @@ before rendering. The rendered prescription is opened in a separate window. It can be saved as PDF or it can be opened in the system browser and printed from there. +### Data + +The prescription files in the default document folder are incorporated into an +index which can be used to quickly find a particular prescription. To use this +feature, select the "Index" option in the "Data" menu. + +Data from the The prescription files can be combined into a table and +exported to a CSV file for analysis from the "Tabular" option in the "Data" +menu. + ### Template The program uses jinja2 template for rendering the prescription. A default @@ -205,10 +214,9 @@ here. 5. Markdown: The markdown formatting can be enabled from here. -6. S/MIME: This is an experimental feature and is not available on every -platform. If it is available, it can be turned on from here. The Private -key, X509 certificate and Root bundle can be selected from the options -that follows this. +6. S/MIME: This is an experimental feature and is disabled by default. To use +it, it has to be enabled first from the settings. The Private key, X509 +certificate and Root bundle can be selected from the options that follow this. License ------- diff --git a/config.py b/config.py index d363eba..82df255 100644 --- a/config.py +++ b/config.py @@ -5,19 +5,12 @@ # MedScript is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along with MedScript. If not, see . -import argparse, json, os, sys, shutil, imp +import argparse, json, os, sys, shutil default_config_file=os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "data", "config.json")) real_dir=os.path.dirname(os.path.realpath(sys.argv[0])) -try: - imp.find_module("M2Crypto") - sign_available=True -except Exception as e: - print(e) - sign_available=False - parser = argparse.ArgumentParser() parser.add_argument("filename", nargs="?") parser.add_argument("-c", "--config") diff --git a/filehandler.py b/filehandler.py index fcc7b06..5af5538 100644 --- a/filehandler.py +++ b/filehandler.py @@ -65,7 +65,7 @@ class FileHandler(): with open(os.path.join(self.directory.name, "prescription.json"), "r") as file: data=file.read() signature=Signature.sign(data, certificate=config["certificate"], privkey=config["private_key"], password=password) - with open(os.path.join(self.directory.name, "signature.p7m"), "w") as file: + with open(os.path.join(self.directory.name, "signature"), "wb") as file: file.write(signature) shutil.copyfile(config["certificate"], os.path.join(self.directory.name, "certificate.pem")) @@ -73,11 +73,11 @@ class FileHandler(): with open(os.path.join(self.directory.name, "prescription.json"), "r") as file: data=file.read() try: - with open(os.path.join(self.directory.name, "certificate.pem")) as file: + with open(os.path.join(self.directory.name, "certificate.pem"), "r") as file: certificate=file.read() - with open(os.path.join(self.directory.name, "signature.p7m")) as file: + with open(os.path.join(self.directory.name, "signature"), "rb") as file: signature=file.read() - return Signature.verify(data, certificate=os.path.join(self.directory.name, "certificate.pem"), signature=os.path.join(self.directory.name, "signature.p7m")) + return Signature.verify(data, certificate=os.path.join(self.directory.name, "certificate.pem"), signature=signature) except FileNotFoundError as e: print(e) @@ -90,7 +90,7 @@ class FileHandler(): def delete_sign(self): try: os.unlink(os.path.join(self.directory.name, "certificate.pem")) - os.unlink(os.path.join(self.directory.name, "signature.p7m")) + os.unlink(os.path.join(self.directory.name, "signature")) except Exception as e: print(e) diff --git a/index.py b/index.py index 59a284b..d14b1f6 100644 --- a/index.py +++ b/index.py @@ -10,7 +10,7 @@ from PyQt6.QtGui import QIcon, QStandardItemModel, QStandardItem from PyQt6.QtCore import pyqtSignal, QSortFilterProxyModel from glob import glob from zipfile import ZipFile -from config import config, config_file, sign_available +from config import config import os, json class Index(QMainWindow): diff --git a/requirements.txt b/requirements.txt index 11ac063..cefae27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ Jinja2==3.1.2 lxml==4.9.2 -M2Crypto==0.38.0 -Markdown==3.4.4 +Markdown==3.5 PyQt6==6.5.2 PyQt6_sip==13.5.2 python_dateutil==2.8.2 diff --git a/setting.py b/setting.py index ca4b4ba..243df9a 100644 --- a/setting.py +++ b/setting.py @@ -9,12 +9,10 @@ from PyQt6.QtWidgets import QWidget, QMainWindow, QFormLayout, QHBoxLayout, QPus from PyQt6.QtGui import QIcon from PyQt6.QtCore import pyqtSignal import os, json -from config import config, config_file, sign_available +from config import config, config_file class EditConfiguration(QMainWindow): - global sign_available - def select_directory(self): d=QFileDialog.getExistingDirectory(self, "Select Directory", config["data_directory"]) if(d): @@ -44,11 +42,10 @@ class EditConfiguration(QMainWindow): self.input_newline.setChecked(bool(self.config["preset_newline"])) self.input_delimiter.setCurrentText(self.config["preset_delimiter"]) self.input_markdown.setChecked(bool(self.config["markdown"])) - if sign_available: - self.input_smime.setChecked(bool(self.config["smime"])) - self.input_key.setText(self.config["private_key"]) - self.input_certificate.setText(self.config["certificate"]) - self.input_root.setText(self.config["root_bundle"]) + self.input_smime.setChecked(bool(self.config["smime"])) + self.input_key.setText(self.config["private_key"]) + self.input_certificate.setText(self.config["certificate"]) + self.input_root.setText(self.config["root_bundle"]) except Exception as e: QMessageBox.critical(self,"Failed to load", "Failed to load the data into the application.") raise(e) @@ -61,11 +58,10 @@ class EditConfiguration(QMainWindow): self.config["preset_newline"]=self.input_newline.isChecked() self.config["preset_delimiter"]=self.input_delimiter.currentText() self.config["markdown"]=self.input_markdown.isChecked() - if sign_available: - self.config["smime"]=self.input_smime.isChecked() - self.config["private_key"]=self.input_key.text() - self.config["certificate"]=self.input_certificate.text() - self.config["root_bundle"]=self.input_root.text() + self.config["smime"]=self.input_smime.isChecked() + self.config["private_key"]=self.input_key.text() + self.config["certificate"]=self.input_certificate.text() + self.config["root_bundle"]=self.input_root.text() with open(config_file, "w") as f: f.write(json.dumps(self.config, indent=4)) QMessageBox.information(self,"Saved", "Configuration saved. Please restart MedScript.") @@ -110,30 +106,29 @@ class EditConfiguration(QMainWindow): layout.addRow("Preset Delimiter", self.input_delimiter) self.input_markdown=QCheckBox("Enable markdown formatting", self) layout.addRow("Markdown", self.input_markdown) - if sign_available: - self.input_smime=QCheckBox("Enable digital signature (experimental)", self) - layout.addRow("S/MIME", self.input_smime) - self.input_key=QLineEdit(self) - btn_key=QPushButton("Select File", self) - btn_key.clicked.connect(self.select_key) - layout_key=QHBoxLayout() - layout_key.addWidget(self.input_key) - layout_key.addWidget(btn_key) - layout.addRow("Private Key", layout_key) - self.input_certificate=QLineEdit(self) - btn_certificate=QPushButton("Select File", self) - btn_certificate.clicked.connect(self.select_certificate) - layout_certificate=QHBoxLayout() - layout_certificate.addWidget(self.input_certificate) - layout_certificate.addWidget(btn_certificate) - layout.addRow("X509 Certificate", layout_certificate) - self.input_root=QLineEdit(self) - btn_root=QPushButton("Select File", self) - btn_root.clicked.connect(self.select_root) - layout_root=QHBoxLayout() - layout_root.addWidget(self.input_root) - layout_root.addWidget(btn_root) - layout.addRow("Root Bundle", layout_root) + self.input_smime=QCheckBox("Enable digital signature (experimental)", self) + layout.addRow("S/MIME", self.input_smime) + self.input_key=QLineEdit(self) + btn_key=QPushButton("Select File", self) + btn_key.clicked.connect(self.select_key) + layout_key=QHBoxLayout() + layout_key.addWidget(self.input_key) + layout_key.addWidget(btn_key) + layout.addRow("Private Key", layout_key) + self.input_certificate=QLineEdit(self) + btn_certificate=QPushButton("Select File", self) + btn_certificate.clicked.connect(self.select_certificate) + layout_certificate=QHBoxLayout() + layout_certificate.addWidget(self.input_certificate) + layout_certificate.addWidget(btn_certificate) + layout.addRow("X509 Certificate", layout_certificate) + self.input_root=QLineEdit(self) + btn_root=QPushButton("Select File", self) + btn_root.clicked.connect(self.select_root) + layout_root=QHBoxLayout() + layout_root.addWidget(self.input_root) + layout_root.addWidget(btn_root) + layout.addRow("Root Bundle", layout_root) button_save=QPushButton("Save") button_save.clicked.connect(self.save) button_reset=QPushButton("Reset") diff --git a/signature.py b/signature.py index 9a06c81..b94e7b4 100644 --- a/signature.py +++ b/signature.py @@ -5,27 +5,26 @@ # MedScript is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along with MedScript. If not, see . -from config import config, sign_available -from hashlib import sha256 +from config import config from datetime import datetime -try: - from M2Crypto import BIO, Rand, SMIME, X509 -except Exception as e: - print(e) - sign_available=False +from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend +from cryptography.x509 import load_pem_x509_certificate class Signature(): def sign(data, certificate, privkey, password=""): - def get_password(*args): - return bytes(password, "ascii") - hash=sha256(data.encode()).hexdigest() - smime=SMIME.SMIME() - smime.load_key(privkey, certificate, get_password) - p7=smime.sign(BIO.MemoryBuffer(hash.encode()), SMIME.PKCS7_DETACHED) - out=BIO.MemoryBuffer() - smime.write(out, p7) - return(out.read().decode()) + with open(privkey, "rb") as f: + try: + priv=load_pem_private_key(f.read(), None, default_backend()) + except TypeError: + priv=load_pem_private_key(f.read(), password.encode(), default_backend()) + with open(certificate, "rb") as f: + cert=load_pem_x509_certificate(f.read()) + signature=priv.sign(data.encode(), padding.PKCS1v15(), hashes.SHA256()) + return signature def verify(data, certificate, signature): try: @@ -35,26 +34,16 @@ class Signature(): print(e) return False - hash=sha256(data.encode()).hexdigest() - smime=SMIME.SMIME() - - x509=X509.load_cert(certificate) - sk=X509.X509_Stack() - sk.push(x509) - smime.set_x509_stack(sk) - - st=X509.X509_Store() - st.load_info(certificate) - smime.set_x509_store(st) - - with open(signature) as file: - buf=BIO.MemoryBuffer(file.read().encode()) - p7=SMIME.smime_load_pkcs7_bio(buf)[0] - + with open(certificate, "rb") as f: + cert=load_pem_x509_certificate(f.read()) + pub=cert.public_key() try: - smime.verify(p7, BIO.MemoryBuffer(hash.encode())) - return(x509.get_subject().as_text()) - except SMIME.PKCS7_Error as e: + pub.verify(signature, data.encode(), padding.PKCS1v15(), hashes.SHA256()) + subattr="" + for i in cert.subject: + subattr+=i.oid._name+":"+i.value+"\n" + return subattr + except Exception as e: print(e) return False @@ -65,23 +54,29 @@ class Signature(): for line in chain_file: cert_data=cert_data+line.strip()+"\n" if "----END CERTIFICATE----" in line: - cert_chain.append(X509.load_cert_string(cert_data)) + cert_chain.append(load_pem_x509_certificate(cert_data.encode())) cert_data="" for i in range(len(cert_chain)): cert=cert_chain[i] - if(datetime.utcnow().timestamp()>cert.get_not_after().get_datetime().timestamp()): + if(datetime.utcnow().timestamp()>cert.not_valid_after.timestamp()): print("Certificate expired") return False if(i>0): prev_cert=cert_chain[i-1] - prev_public_key=prev_cert.get_pubkey().get_rsa() - if(not prev_cert.verify(cert.get_pubkey())): + try: + cert.public_key().verify(prev_cert.signature, prev_cert.tbs_certificate_bytes, padding.PKCS1v15(), prev_cert.signature_hash_algorithm) + except InvalidSignature: print("Certificate chain signature verification failed") return False - with open(config["root_bundle"]) as root: - root_bundle=root.read() - if(cert_chain[-1].as_pem().decode() not in root_bundle): - print("Certificate not in root bundle") + try: + with open(config["root_bundle"]) as root: + root_bundle=root.read() + if(cert_chain[-1].public_bytes(encoding=Encoding.PEM).decode() not in root_bundle): + print("Certificate not in root bundle") + return False + return True + except Exception as e: + print(e) + print("Root bundle could not be loaded") return False - return True diff --git a/window.py b/window.py index 0b7abad..20c348d 100644 --- a/window.py +++ b/window.py @@ -12,7 +12,7 @@ from PyQt6.QtGui import QAction, QIcon from pathlib import Path from hashlib import md5 -from config import config, real_dir, sign_available +from config import config, real_dir from prescription import Prescription from renderer import Renderer from filehandler import FileHandler @@ -22,13 +22,6 @@ from viewbox import ViewBox from preset import Preset from tabular import Tabular from index import Index -try: - from M2Crypto.EVP import EVPError - from M2Crypto.BIO import BIOError - from M2Crypto.SMIME import SMIME_Error -except Exception as e: - print(e) - sign_available=False class MainWindow(QMainWindow): @@ -123,11 +116,12 @@ class MainWindow(QMainWindow): def cmd_sign(self): self.refresh() if(self.save_state==md5(self.prescription.get_json().encode()).hexdigest()): - password, ok=QInputDialog.getText(self, "Enter password", "Private key password", QLineEdit.EchoMode.Password) + ok=True #password, ok=QInputDialog.getText(self, "Enter password", "Private key password", QLineEdit.EchoMode.Password) if(ok): try: try: - self.current_file.sign(password) + self.current_file.sign() + #self.current_file.sign(password) self.cmd_save() except FileNotFoundError as e: print(e) @@ -294,7 +288,7 @@ class MainWindow(QMainWindow): def load_interface(self, file="", date=None, id="", name="", age="", sex="", address="", contact="", extra="", mode="", daw="", diagnosis="", note="", report="", advice="", investigation="", medication="", additional=""): try: file_msg=self.current_file.file if self.current_file.file else "New file" - sign_msg="(signed)" if sign_available and config["smime"] and self.current_file.is_signed() else "" + sign_msg="(signed)" if config["smime"] and self.current_file.is_signed() else "" self.statusbar.showMessage(file_msg+" "+sign_msg) if date is None: d=QDateTime.currentDateTime() @@ -427,8 +421,6 @@ class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - global sign_available - self.setWindowTitle("MedScript") self.setGeometry(100, 100, 600, 400) self.setWindowIcon(QIcon(os.path.join(config["resource"], "icon_medscript.ico"))) @@ -507,7 +499,7 @@ class MainWindow(QMainWindow): menu_prepare=menubar.addMenu("Prepare") menu_prepare.addAction(action_render) menu_prepare.addAction(action_refresh) - if(sign_available and config["smime"]): + if(config["smime"]): menu_prepare.addAction(action_sign) menu_prepare.addAction(action_unsign) menu_prepare.addAction(action_verify) @@ -516,8 +508,8 @@ class MainWindow(QMainWindow): menu_settings.addAction(action_prescriber) menu_settings.addAction(action_switch) menu_data=menubar.addMenu("Data") - menu_data.addAction(action_tabular) menu_data.addAction(action_index) + menu_data.addAction(action_tabular) menu_help=menubar.addMenu("Help") menu_help.addAction(action_about) menu_help.addAction(action_help)