]> Softwares of Agnibho - medscript.git/commitdiff
Reimplmented prescription signing with cryptography module
authorAgnibho Mondal <mondal@agnibho.com>
Thu, 12 Oct 2023 18:38:41 +0000 (00:08 +0530)
committerAgnibho Mondal <mondal@agnibho.com>
Thu, 12 Oct 2023 18:38:41 +0000 (00:08 +0530)
README
config.py
filehandler.py
index.py
requirements.txt
setting.py
signature.py
window.py

diff --git a/README b/README
index eaf0337c9139043e81437650fe1bd100205dd63e..b879ea8d6ccf3833b96419108d8a844abd210fc0 100644 (file)
--- 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
 -------
index d363eba23edeeac1d2f3c53c8f105e1d954264da..82df25558da19bc4409925dd7d660dbfa5a23ef9 100644 (file)
--- 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 <https://www.gnu.org/licenses/>.
 
-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")
index fcc7b062e6ce0b1652f86d71a475fec8ae03dd86..5af553800ece9f581c562eeb45c4ccc4f1c0f663 100644 (file)
@@ -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)
 
index 59a284b61cb0ce66383ac3e55cb89d5a7feb822e..d14b1f67edd01655f8ef134fe3bce637589a5623 100644 (file)
--- 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):
index 11ac06374a8cc1155064b4cf53782dbe1bd26b64..cefae2776d8610f650264b4c22bf3ed329aca1a9 100644 (file)
@@ -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
index ca4b4ba59c484fd314a18f840714d6296c78f288..243df9a882d0993493841ca5574227caaed64782 100644 (file)
@@ -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")
index 9a06c81df23654ed7d63715192384399350f8e95..b94e7b436ac8be06f82633ee8b8f17f8826f7b49 100644 (file)
@@ -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 <https://www.gnu.org/licenses/>.
 
-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
index 0b7abad84eca10b941e9d8ab8a7b4bca06e24a6f..20c348d7f0cce4b98cbd0d5ec057b919a0882827 100644 (file)
--- 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)