4. Jinja2
5. lxml
6. Markdown
-7. M2Crypto (optional)
Usage
-----
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
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
-------
# 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")
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"))
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)
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)
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):
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
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):
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)
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.")
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")
# 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:
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
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
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
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):
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)
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()
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")))
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)
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)