Skip to content

Commit

Permalink
Added file encryption capability
Browse files Browse the repository at this point in the history
Added tests
ran all unit tests - pass
checked coverage - full coverage of Encryption

fixed tests

and allowed for uninstalled packages
  • Loading branch information
PetePupalaikis committed Aug 17, 2023
1 parent a3a8da3 commit d82311b
Show file tree
Hide file tree
Showing 31 changed files with 9,795 additions and 106 deletions.
9 changes: 6 additions & 3 deletions SignalIntegrity/App/Archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(self):
list.__init__(self,[])
def Archivable(self):
return self != []
def BuildArchiveDictionary(self,parent):
def BuildArchiveDictionary(self,parent,external=False):
import SignalIntegrity.App.Project
from SignalIntegrity.App.SignalIntegrityAppHeadless import SignalIntegrityAppHeadless
currentPath=os.getcwd()
Expand All @@ -47,10 +47,13 @@ def BuildArchiveDictionary(self,parent):
thisFile=parent
app=SignalIntegrityAppHeadless()
app.projectStack.Push()
fileargs=SignalIntegrity.App.Project['Variables'].Dictionary()
if external:
external=False
fileargs={}
else:
fileargs=SignalIntegrity.App.Project['Variables'].Dictionary()
if not app.OpenProjectFile(thisFile,args=fileargs):
app.projectStack.Pull()
list.__init__(self,[])
return self

initial=True
Expand Down
4 changes: 3 additions & 1 deletion SignalIntegrity/App/DeviceProperties.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ def fileTreatment(value,typeString):
if useCalculationProperties:
calculationProperties=SignalIntegrity.App.Project['CalculationProperties']
kwPairs+=' '+' '.join([propertyName+' '+str(calculationProperties[propertyName]) for propertyName in ['EndFrequency','FrequencyPoints','UserSampleRate']])
result=os.system('SignalIntegrity "'+os.path.abspath(filename)+'" --external '+kwPairs)
from SignalIntegrity.App.Encryption import Encryption
pwdArgString = '' if Encryption.password == None else ' --pwd "'+Encryption.password+'" '
result=os.system('SignalIntegrity "'+os.path.abspath(filename)+'"'+pwdArgString+' --external '+kwPairs)
if result != 0:
messagebox.showerror('ProjectFile','could not be opened')
return
Expand Down
113 changes: 113 additions & 0 deletions SignalIntegrity/App/Encryption.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Encryption.py
"""
# Copyright (c) 2018 Teledyne LeCroy, Inc.
# All rights reserved worldwide.
#
# This file is part of SignalIntegrity.
#
# SignalIntegrity is free software: You can redistribute it and/or modify it under the terms
# of the GNU General Public License as published by the Free Software Foundation, either
# version 3 of the License, or any later version.
#
# This program 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 this program.
# If not, see <https://www.gnu.org/licenses/>
import os

class Encryption(object):
"""
https://cryptobook.nakov.com/symmetric-key-ciphers/aes-encrypt-decrypt-examples
while this is not added to the setup.py (the encryption feature is probably needed
by only a few, in order to use it, you need to pip install:
pycryptodome
scrypt
"""
password = None
ending = '$'
Save=None
def __init__(self,pwd=None,ending=None):
if pwd != None:
Encryption.password = pwd
if ending != None:
if ending == '' or Encryption.ending in ['',None]:
ending = '$'
Encryption.ending = ending

def __encrypt_AES_GCM(self,msg, password=None):
from Crypto.Cipher import AES
import scrypt
if password == None: password=self.password.encode()
kdfSalt = os.urandom(16)
secretKey = scrypt.hash(password, kdfSalt, N=16384, r=8, p=1, buflen=32)
aesCipher = AES.new(secretKey, AES.MODE_GCM)
ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
return (kdfSalt, ciphertext, aesCipher.nonce, authTag)

def __decrypt_AES_GCM(self,encryptedMsg, password=None):
from Crypto.Cipher import AES
import scrypt
if password == None: password=self.password.encode()
(kdfSalt, ciphertext, nonce, authTag) = encryptedMsg
secretKey = scrypt.hash(password, kdfSalt, N=16384, r=8, p=1, buflen=32)
aesCipher = AES.new(secretKey, AES.MODE_GCM, nonce)
plaintext = aesCipher.decrypt_and_verify(ciphertext, authTag)
return plaintext

def Decrypt(self,text):
import binascii
kdfSalt=binascii.unhexlify(text[0:32])
aesIV=binascii.unhexlify(text[32:64])
authTag=binascii.unhexlify(text[64:96])
ciphertext=binascii.unhexlify(text[96:])
encryptedMsg=(kdfSalt, ciphertext, aesIV, authTag)
plaintext=self.__decrypt_AES_GCM(encryptedMsg)
return plaintext

def Encrypt(self,text):
import binascii
(kdfSalt, ciphertext, aesIV, authTag)=self.__encrypt_AES_GCM(text, self.password)
Encryption.Save=(kdfSalt, ciphertext, aesIV, authTag)
kdfSaltText=binascii.hexlify(kdfSalt)
ciphertextText=binascii.hexlify(ciphertext)
aesIVText=binascii.hexlify(aesIV)
authtagText=binascii.hexlify(authTag)
return kdfSaltText+aesIVText+authtagText+ciphertextText

def WriteEncryptedLines(self,filename,lines):
textToWrite=''.join(lines)
if self.password != None and os.path.splitext(filename)[0].endswith(self.ending):
try:
textToWrite=self.Encrypt(textToWrite.encode()).decode()
except ModuleNotFoundError: # pragma: no cover
raise IOError('file cannot be encrypted')
with open(filename,'w') as f:
f.write(textToWrite)
return self

def ReadEncryptedLines(self,filename,split=True):
with open(filename,'r') as f:
text=f.readlines()
if len(text)==1:
if self.password != None:
try:
text=self.Decrypt(text[0].encode()).decode()
if split:
text=text.splitlines(True)
except:
raise IOError('decryption failed')
else:
raise IOError('file is encrypted')
return text

if __name__ == '__main__': # pragma: no cover
pwd=Encryption(pwd='test',ending='$')
lines=['This is a test\n','of the emergency broadcast system\n']
pwd.WriteEncryptedLines('test$.txt',lines)
print(pwd.ReadEncryptedLines('test$.txt'))
19 changes: 9 additions & 10 deletions SignalIntegrity/App/HeaderDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from SignalIntegrity.App.FilePicker import AskSaveAsFilename,AskOpenFileName
from SignalIntegrity.App.MenuSystemHelpers import Doer
from SignalIntegrity.App.Files import FileParts
from SignalIntegrity.App.Encryption import Encryption

class HeaderDialog(tk.Toplevel):
def __init__(self,parent, titleName=None, fileparts=None):
Expand All @@ -38,16 +39,14 @@ def __init__(self,parent, titleName=None, fileparts=None):
lines=''
width=0
try:
with open(fileparts.FullFilePathExtension(),'rt') as f:
keepGoing = True
while keepGoing:
line = f.readline()
if line[0] in ['!',' ','#','\n']:
if line[0] == '!':
lines=lines+line[1:-1]+'\n'
width=max(len(line),width)
else:
keepGoing = False
spFileLines=Encryption().ReadEncryptedLines(fileparts.FullFilePathExtension())
for line in spFileLines:
if line[0] in ['!',' ','#','\n']:
if line[0] == '!':
lines=lines+line[1:-1]+'\n'
width=max(len(line),width)
else:
break
except:
return
elif isinstance(fileparts,list):
Expand Down
2 changes: 2 additions & 0 deletions SignalIntegrity/App/MenuSystemHelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,11 @@ def __init__(self, master):
def set(self, format, *args):
self.label.config(text=format % args)
self.label.update_idletasks()
self.label.update()
def clear(self):
self.label.config(text="")
self.label.update_idletasks()
self.label.update()

class ScrollableFrame(ttk.Frame):
def __init__(self, container, *args, **kwargs):
Expand Down
16 changes: 16 additions & 0 deletions SignalIntegrity/App/PreferencesDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# If not, see <https://www.gnu.org/licenses/>
from SignalIntegrity.App.CalculationPropertiesProject import PropertiesDialog,CalculationProperty,CalculationPropertyTrueFalseButton,CalculationPropertyColor,CalculationPropertySI
from SignalIntegrity.App.BuildHelpSystem import HelpSystemKeys
from SignalIntegrity.App.Encryption import Encryption

class PreferencesDialog(PropertiesDialog):
def __init__(self, parent,preferences):
Expand All @@ -43,9 +44,12 @@ def __init__(self, parent,preferences):
self.preferLeCroyWaveform=CalculationPropertyTrueFalseButton(self.propertyListFrame,'prefer saving waveforms in LeCroy format',None,self.onUpdatePreferences,preferences,'ProjectFiles.PreferSaveWaveformsLeCroyFormat')
self.cacheResult=CalculationPropertyTrueFalseButton(self.propertyListFrame,'cache results',None,self.onUpdatePreferences,preferences,'Cache.CacheResults')
self.parameterizeVisible=CalculationPropertyTrueFalseButton(self.propertyListFrame,'parameterize visible properties only',None,self.onUpdatePreferences,preferences,'Variables.ParameterizeOnlyVisible')
self.encryptionPassword = CalculationProperty(self.propertyListFrame,'Password for encryption',None,self.onUpdatePassword,preferences,'ProjectFiles.Encryption.Password')
self.encryptionEnding = CalculationProperty(self.propertyListFrame,'File ending for encryption',None,self.onUpdatePassword,preferences,'ProjectFiles.Encryption.Ending')
self.useOnlineHelp=CalculationPropertyTrueFalseButton(self.propertyListFrame,'use online help',None,self.onUpdatePreferences,preferences,'OnlineHelp.UseOnlineHelp')
self.onlineHelpURL=CalculationProperty(self.propertyListFrame,'online help url',None,self.onUpdatePreferences,preferences,'OnlineHelp.URL')
self.Finish()

def onUpdatePreferences(self):
self.onlineHelpURL.Show(self.project['OnlineHelp.UseOnlineHelp'])
self.project.SaveToFile()
Expand All @@ -54,6 +58,18 @@ def onUpdatePreferences(self):
def onUpdateColors(self):
self.parent.UpdateColorsAndFonts()
self.onUpdatePreferences()

def onUpdatePassword(self):
pwd = self.project['ProjectFiles.Encryption.Password']
if pwd in ['','None',None]:
pwd = None
ending = self.project['ProjectFiles.Encryption.Ending']
if ending in ['','None',None]:
ending = '$'
self.project['ProjectFiles.Encryption.Ending'] = ending
Encryption(pwd=pwd,ending=ending)
self.onUpdatePreferences()

def Finish(self):
self.onlineHelpURL.Show(self.project.GetValue('OnlineHelp.UseOnlineHelp'))
PropertiesDialog.Finish(self)
13 changes: 13 additions & 0 deletions SignalIntegrity/App/PreferencesFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ def __init__(self):
self.Add(XMLPropertyDefaultString('Name'))
self.Add(XMLPropertyDefaultString('Directory'))

class Encryption(XMLConfiguration):
def __init__(self):
XMLConfiguration.__init__(self,'Encryption')
self.Add(XMLPropertyDefaultString('Password',''))
self.Add(XMLPropertyDefaultString('Ending','$'))
def ApplyPreferences(self):
from SignalIntegrity.App.Encryption import Encryption
Encryption(pwd=self['Password'],ending=self['Ending'])

class ProjectFiles(XMLConfiguration):
def __init__(self):
XMLConfiguration.__init__(self,'ProjectFiles')
Expand All @@ -194,6 +203,7 @@ def __init__(self):
self.Add(XMLProperty('LastFile',[LastFiles() for _ in range(4)],'array',arrayType=LastFiles()))
self.Add(XMLPropertyDefaultBool('AskToSaveCurrentFile',True))
self.Add(XMLPropertyDefaultBool('PreferSaveWaveformsLeCroyFormat',False))
self.SubDir(Encryption())

class OnlineHelp(XMLConfiguration):
def __init__(self):
Expand All @@ -219,4 +229,7 @@ def __init__(self):
self.SubDir(DeviceConfigurations())
self.SubDir(Variables())
self.SubDir(Features())
def ApplyPreferences(self):
self['Calculation'].ApplyPreferences()
self['ProjectFiles.Encryption'].ApplyPreferences()

34 changes: 22 additions & 12 deletions SignalIntegrity/App/ProjectFileBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# You should have received a copy of the GNU General Public License along with this program.
# If not, see <https://www.gnu.org/licenses/>
import xml.etree.ElementTree as et
import os
from SignalIntegrity.App.Encryption import Encryption

class XMLProperty(object):
def __init__(self,propertyName,propertyValue=None,propertyType=None,write=True,arrayType=None):
Expand Down Expand Up @@ -325,10 +327,10 @@ def LinesToWrite(self):
return lines

def LinesInFile(self,filename):
if not filename.split('.')[-1] == self.ext:
filename=filename+'.'+self.ext
with open(filename,'r') as f:
lines=f.readlines()
filenamebase,ext=os.path.splitext(filename)
if ext != '.'+self.ext:
filename=filenamebase+'.'+self.ext
lines=Encryption().ReadEncryptedLines(filename,split=True)
return lines

def CheckFileChanged(self,filename):
Expand Down Expand Up @@ -359,22 +361,30 @@ def CheckFileChanged(self,filename):
except:
return True


def Write(self,filename):
if not filename.split('.')[-1] == self.ext:
filename=filename+'.'+self.ext
with open(filename,'w') as f:
f.writelines(self.LinesToWrite())
filenamebase,ext=os.path.splitext(filename)
if ext != '.'+self.ext:
filename=filenamebase+'.'+self.ext
Encryption().WriteEncryptedLines(filename,self.LinesToWrite())
return self

def Read(self,filename):
if not filename.split('.')[-1] == self.ext:
filename=filename+'.'+self.ext
tree=et.parse(filename)
def FromText(self,text):
xmlstring=''.join(text)
tree=et.ElementTree(et.fromstring(xmlstring))
root=tree.getroot()
self.Parse(root)
self.SetChanged(True)
return self

def Read(self,filename):
filenamebase,ext=os.path.splitext(filename)
if ext != '.'+self.ext:
filename=filenamebase+'.'+self.ext
text=Encryption().ReadEncryptedLines(filename,split=False)
self.FromText(text)
return self

def Parse(self,element):
for child in element:
try:
Expand Down
42 changes: 27 additions & 15 deletions SignalIntegrity/App/SignalIntegrityApp.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@
from SignalIntegrity.App.InformationMessage import InformationMessage
from SignalIntegrity.App.VariablesDialog import VariablesDialog
from SignalIntegrity.App.ProjectFile import VariableConfiguration
from SignalIntegrity.App.Encryption import Encryption

from SignalIntegrity.__about__ import __version__,__project__
import SignalIntegrity.App.Project

class SignalIntegrityApp(tk.Frame):
def __init__(self,projectFileName=None,runMainLoop=True,external=False,args={}):
def __init__(self,projectFileName=None,pwd=None,runMainLoop=True,external=False,args={}):
thisFileDir=os.path.dirname(os.path.realpath(__file__))
sys.path=[thisFileDir]+sys.path

Expand Down Expand Up @@ -330,6 +331,18 @@ def __init__(self,projectFileName=None,runMainLoop=True,external=False,args={}):
SignalIntegrity.App.Project=ProjectFile()
self.Drawing.InitFromProject()

if pwd == None:
pwd = SignalIntegrity.App.Preferences['ProjectFiles.Encryption.Password']
if pwd in ['','None',None]:
pwd = None
ending = SignalIntegrity.App.Preferences['ProjectFiles.Encryption.Ending']
if ending in ['','None',None]:
ending = '$'
SignalIntegrity.App.Preferences['ProjectFiles.Encryption.Ending'] = ending
SignalIntegrity.App.Preferences.SaveToFile()

Encryption(pwd=pwd,ending=ending)

# The Simulator Dialog
self.simulator = Simulator(self)
self.networkanalyzersimulator = NetworkAnalyzerSimulator(self)
Expand Down Expand Up @@ -1461,20 +1474,19 @@ def onUnExtractArchive(self):
msg=messagebox.showinfo('Archive Unextract','Archive Unextracted')

def main():
projectFileName = None
external=False
argsDict={}
if len(sys.argv) >= 2:
projectFileName=sys.argv[1]

if len(sys.argv) >= 3:
if sys.argv[2] == '--external':
external = True

argsDict=dict(zip(sys.argv[3::2],sys.argv[4::2]))

SignalIntegrityApp(projectFileName,external=external,args=argsDict)

import argparse
parser = argparse.ArgumentParser(
prog='SignalIntegrity',
description='Signal and Power Integrity Tools',
epilog='SignalIntegrity')
parser.add_argument('filename',nargs='?',default=None) # positional argument
parser.add_argument('-pwd', '--pwd') # option that takes a value
parser.add_argument('-e', '--external', action='store_true') # on/off flag
args, unknown = parser.parse_known_args()

argsDict=dict(zip(unknown[0::2],unknown[1::2]))
SignalIntegrityApp(args.filename,pwd=args.pwd,external=args.external,args=argsDict)

if __name__ == '__main__': # pragma: no cover
runProfiler=False

Expand Down
Loading

0 comments on commit d82311b

Please sign in to comment.