Update Picocrypt.py

This commit is contained in:
Evan Su 2021-03-23 18:55:57 -04:00 committed by GitHub
parent d101b779f5
commit a2ab95ec53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 417 additions and 232 deletions

View File

@ -1,34 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Dependencies: argon2-cffi, pycryptodome, reedsolo
Picocrypt v1.11 (Beta)
Dependencies: argon2-cffi, pycryptodome, reedsolo, tkinterdnd2
Copyright (c) Evan Su (https://evansu.cc) Copyright (c) Evan Su (https://evansu.cc)
Released under a GNU GPL v3 License Released under a GNU GPL v3 License
https://github.com/HACKERALERT/Picocrypt https://github.com/HACKERALERT/Picocrypt
~ In cryptography we trust ~
""" """
# Test if libraries are installed
try:
from argon2.low_level import hash_secret_raw
from Crypto.Cipher import ChaCha20_Poly1305
try:
from creedsolo import ReedSolomonError
except:
from reedsolo import ReedSolomonError
except:
# Libraries missing, install them
from os import system
try:
# Debian/Ubuntu based
system("sudo apt-get install python3-tk")
except:
# Fedora
system("sudo dnf install python3-tkinter")
system("python3 -m pip install argon2-cffi --no-cache-dir")
system("python3 -m pip install pycryptodome --no-cache-dir")
system("python3 -m pip install reedsolo --no-cache-dir")
# Imports # Imports
from tkinter import filedialog,messagebox from tkinter import filedialog,messagebox
from threading import Thread from threading import Thread
@ -37,17 +20,22 @@ from argon2.low_level import hash_secret_raw,Type
from Crypto.Cipher import ChaCha20_Poly1305 from Crypto.Cipher import ChaCha20_Poly1305
from Crypto.Hash import SHA3_512 as sha3_512 from Crypto.Hash import SHA3_512 as sha3_512
from secrets import compare_digest from secrets import compare_digest
from os import urandom,fsync,remove from os import urandom,fsync,remove,system
from os.path import getsize,expanduser from os.path import getsize,expanduser,isdir
from os.path import dirname,abspath,realpath
from os.path import join as pathJoin
from os.path import split as pathSplit
from tkinterdnd2 import TkinterDnD,DND_FILES
from zipfile import ZipFile
from pathlib import Path
from shutil import rmtree
import sys import sys
import tkinter import tkinter
import tkinter.ttk import tkinter.ttk
import tkinter.scrolledtext import tkinter.scrolledtext
import webbrowser import webbrowser
try: import platform
from creedsolo import RSCodec,ReedSolomonError from creedsolo import RSCodec,ReedSolomonError
except:
from reedsolo import RSCodec,ReedSolomonError
# Tk/Tcl is a little barbaric, so I'm disabling # Tk/Tcl is a little barbaric, so I'm disabling
# high DPI so it doesn't scale bad and look horrible # high DPI so it doesn't scale bad and look horrible
@ -57,16 +45,22 @@ try:
except: except:
pass pass
# Global variables and notices # Global variables and strings
rootDir = dirname(realpath(__file__))
inputFile = "" inputFile = ""
outputFile = "" outputFile = ""
outputPath = ""
password = "" password = ""
ad = "" ad = ""
kept = False kept = False
working = False working = False
gMode = None gMode = None
headerRsc = None headerRsc = False
allFiles = False
draggedFolderPaths = False
files = False
adString = "File metadata (used to store some text along with the file):" adString = "File metadata (used to store some text along with the file):"
compressingNotice = "Compressing files together..."
passwordNotice = "Error. The provided password is incorrect." passwordNotice = "Error. The provided password is incorrect."
corruptedNotice = "Error. The input file is corrupted." corruptedNotice = "Error. The input file is corrupted."
veryCorruptedNotice = "Error. The input file and header keys are badly corrupted." veryCorruptedNotice = "Error. The input file and header keys are badly corrupted."
@ -77,16 +71,21 @@ kVeryCorruptedNotice = "The input file is badly corrupted, but the output has be
derivingNotice = "Deriving key (takes a few seconds)..." derivingNotice = "Deriving key (takes a few seconds)..."
keepNotice = "Keep decrypted output even if it's corrupted or modified" keepNotice = "Keep decrypted output even if it's corrupted or modified"
eraseNotice = "Securely erase and delete original file" eraseNotice = "Securely erase and delete original file"
erasingNotice = "Securely erasing original file(s)..."
overwriteNotice = "Output file already exists. Would you like to overwrite it?" overwriteNotice = "Output file already exists. Would you like to overwrite it?"
cancelNotice = "Exiting now will lead to broken output. Are you sure?"
rsNotice = "Prevent corruption using Reed-Solomon" rsNotice = "Prevent corruption using Reed-Solomon"
rscNotice = "Creating Reed-Solomon tables..." rscNotice = "Creating Reed-Solomon tables..."
unknownErrorNotice = "Unknown error occured. Please try again." unknownErrorNotice = "Unknown error occured. Please try again."
# Create root Tk # Create root Tk
tk = tkinter.Tk() tk = TkinterDnD.Tk()
tk.geometry("480x480") tk.geometry("480x470")
tk.title("Picocrypt") tk.title("Picocrypt")
tk.configure(background="#f5f6f7") if platform.system()=="Darwin":
tk.configure(background="#edeced")
else:
tk.configure(background="#ffffff")
tk.resizable(0,0) tk.resizable(0,0)
# Try setting window icon if included with Picocrypt # Try setting window icon if included with Picocrypt
@ -98,29 +97,85 @@ except:
# Some styling # Some styling
s = tkinter.ttk.Style() s = tkinter.ttk.Style()
s.configure("TCheckbutton",background="#f5f6f7") s.configure("TCheckbutton",background="#ffffff")
# Event when user selects an input file # Event when user drags file(s) and folder(s) into window
def inputSelected(): def inputSelected(draggedFile):
global inputFile,working,headerRsc global inputFile,working,headerRsc,allFiles,draggedFolderPaths,files
resetUI()
dummy.focus() dummy.focus()
status.config(cursor="")
status.bind("<Button-1>",lambda e:None)
# Try to handle when select file is cancelled # Use try to handle errors
try: try:
# Ask for input file # Create list of input files
allFiles = []
files = []
draggedFolderPaths = []
suffix = "" suffix = ""
tmp = filedialog.askopenfilename( tmp = [i for i in draggedFile]
initialdir=expanduser("~") res = []
) within = False
if len(tmp)==0: tmpName = ""
# Exception will be caught by except below
raise Exception("No file selected.") """
inputFile = tmp The next for loop parses data return by tkinterdnd2's file drop method.
When files and folders are dragged, the output (the 'draggedFile' parameter)
will contain the dropped files/folders and will look something like this:
A single file/folder: "C:\Foo\Bar.txt"
A single file/folder with a space in path: "{C:\Foo Bar\Lorem.txt}"
Multiple files/folders: "C:\Foo\Bar1.txt C:\Foo\Ba2.txt"
Multiple files/folders with spaces in paths:
- "C:\Foo\Bar1.txt {C:\Foo Bar\Lorem.txt}"
- "{C:\Foo Bar\Lorem.txt} C:\Foo\Bar1.txt"
- "{C:\Foo Bar\Lorem1.txt} {C:\Foo Bar\Lorem2.txt}"
"""
for i in tmp:
if i=="{":
within = True
elif i=="}":
within = False
res.append(tmpName)
tmpName = ""
else:
if i==" " and not within:
if tmpName!="":
res.append(tmpName)
tmpName = ""
else:
tmpName += i
if tmpName:
res.append(tmpName)
allFiles = []
files = []
# Check each thing dragged by user
for i in res:
# If there is a directory, recursively add all files to 'allFiles'
if isdir(i):
# Record the directory for secure wipe (if necessary)
draggedFolderPaths.append(i)
tmp = Path(i).rglob("*")
for p in tmp:
allFiles.append(abspath(p))
# Just a file, add it to files
else:
files.append(i)
# If there's only one file, set it as input file
if len(files)==1 and len(allFiles)==0:
inputFile = files[0]
files = []
else:
inputFile = ""
# Decide if encrypting or decrypting # Decide if encrypting or decrypting
if ".pcv" in inputFile.split("/")[-1]: if inputFile.endswith(".pcv"):
suffix = " (will decrypt)" suffix = " (will decrypt)"
fin = open(inputFile,"r+b") fin = open(inputFile,"rb")
# Read file metadata (a little complex) # Read file metadata (a little complex)
tmp = fin.read(139) tmp = fin.read(139)
@ -148,6 +203,8 @@ def inputSelected():
adArea.delete("1.0",tkinter.END) adArea.delete("1.0",tkinter.END)
adArea.insert("1.0",ad) adArea.insert("1.0",ad)
adArea["state"] = "disabled" adArea["state"] = "disabled"
# Update UI
adLabelString.set("File metadata (read only):") adLabelString.set("File metadata (read only):")
keepBtn["state"] = "normal" keepBtn["state"] = "normal"
eraseBtn["state"] = "disabled" eraseBtn["state"] = "disabled"
@ -155,6 +212,7 @@ def inputSelected():
cpasswordInput["state"] = "normal" cpasswordInput["state"] = "normal"
cpasswordInput.delete(0,"end") cpasswordInput.delete(0,"end")
cpasswordInput["state"] = "disabled" cpasswordInput["state"] = "disabled"
cpasswordString.set("Confirm password (N/A):")
else: else:
# Update the UI # Update the UI
eraseBtn["state"] = "normal" eraseBtn["state"] = "normal"
@ -166,13 +224,33 @@ def inputSelected():
adLabelString.set(adString) adLabelString.set(adString)
cpasswordInput["state"] = "normal" cpasswordInput["state"] = "normal"
cpasswordInput.delete(0,"end") cpasswordInput.delete(0,"end")
cpasswordString.set("Confirm password:")
cpasswordLabel["state"] = "normal"
adLabel["state"] = "normal"
nFiles = len(files)
nFolders = len(draggedFolderPaths)
# Show selected file(s) and folder(s)
if (allFiles or files) and not draggedFolderPaths:
inputString.set(f"{nFiles} files selected (will encrypt).")
elif draggedFolderPaths and not files:
inputString.set(f"{nFolders} folder{'s' if nFolders!=1 else ''} selected (will encrypt).")
elif draggedFolderPaths and (allFiles or files):
inputString.set(
f"{nFiles} file{'s' if nFiles!=1 else ''} and "+
f"{nFolders} folder{'s' if nFolders!=1 else ''} selected (will encrypt)."
)
else:
inputString.set(inputFile.split("/")[-1]+suffix)
# Enable password box, etc. # Enable password box, etc.
inputString.set(inputFile.split("/")[-1]+suffix)
passwordInput["state"] = "normal" passwordInput["state"] = "normal"
passwordInput.delete(0,"end") passwordInput.delete(0,"end")
passwordLabel["state"] = "normal"
startBtn["state"] = "normal" startBtn["state"] = "normal"
statusString.set("Ready.") statusString.set("Ready.")
status["state"] = "enabled"
progress["value"] = 0 progress["value"] = 0
# File decode error # File decode error
@ -180,32 +258,54 @@ def inputSelected():
statusString.set(corruptedNotice) statusString.set(corruptedNotice)
progress["value"] = 100 progress["value"] = 100
# No file selected, do nothing # No file(s) selected, do nothing
except: except:
pass inputString.set("Drag and drop file(s) and folder(s) into this window.")
resetUI()
# Focus the dummy button to remove ugly borders # Focus the dummy button to remove ugly borders
finally: finally:
dummy.focus() dummy.focus()
working = False working = False
# Button to select input file # Clears the selected files
selectFileInput = tkinter.ttk.Button( def clearInputs():
tk, dummy.focus()
text="Select file", resetUI()
command=inputSelected,
) # Allow drag and drop
selectFileInput.place(x=19,y=20) def onDrop(e):
global working
if not working:
inputSelected(e.data)
tk.drop_target_register(DND_FILES)
tk.dnd_bind("<<Drop>>",onDrop)
# Label that displays selected input file # Label that displays selected input file
inputString = tkinter.StringVar(tk) inputString = tkinter.StringVar(tk)
inputString.set("Please select a file.") inputString.set("Drag and drop file(s) and folder(s) into this window.")
selectedInput = tkinter.ttk.Label( selectedInput = tkinter.ttk.Label(
tk, tk,
textvariable=inputString textvariable=inputString
) )
selectedInput.config(background="#f5f6f7") selectedInput.config(background="#ffffff")
selectedInput.place(x=104,y=23) selectedInput.place(x=17,y=16)
# Clear input files
clearInput = tkinter.ttk.Button(
tk,
text="Clear",
command=clearInputs
)
if platform.system()=="Darwin":
clearInput.place(x=398,y=14,width=64,height=24)
else:
clearInput.place(x=421,y=14,width=40,height=24)
separator = tkinter.ttk.Separator(
tk
)
separator.place(x=20,y=36,width=440)
# Label that prompts user to enter a password # Label that prompts user to enter a password
passwordString = tkinter.StringVar(tk) passwordString = tkinter.StringVar(tk)
@ -214,16 +314,17 @@ passwordLabel = tkinter.ttk.Label(
tk, tk,
textvariable=passwordString textvariable=passwordString
) )
passwordLabel.place(x=17,y=56) passwordLabel.place(x=17,y=46)
passwordLabel.config(background="#f5f6f7") passwordLabel.config(background="#ffffff")
passwordLabel["state"] = "disabled"
# A frame to make password input fill width # A frame to make password input fill width
passwordFrame = tkinter.Frame( passwordFrame = tkinter.Frame(
tk, tk,
width=440, width=(445 if platform.system()=="Darwin" else 440),
height=22 height=22
) )
passwordFrame.place(x=20,y=76) passwordFrame.place(x=(17 if platform.system()=="Darwin" else 20),y=66)
passwordFrame.columnconfigure(0,weight=10) passwordFrame.columnconfigure(0,weight=10)
passwordFrame.grid_propagate(False) passwordFrame.grid_propagate(False)
# Password input box # Password input box
@ -240,16 +341,17 @@ cpasswordLabel = tkinter.ttk.Label(
tk, tk,
textvariable=cpasswordString textvariable=cpasswordString
) )
cpasswordLabel.place(x=17,y=106) cpasswordLabel.place(x=17,y=96)
cpasswordLabel.config(background="#f5f6f7") cpasswordLabel.config(background="#ffffff")
cpasswordLabel["state"] = "disabled"
# A frame to make confirm password input fill width # A frame to make confirm password input fill width
cpasswordFrame = tkinter.Frame( cpasswordFrame = tkinter.Frame(
tk, tk,
width=440, width=(445 if platform.system()=="Darwin" else 440),
height=22 height=22
) )
cpasswordFrame.place(x=20,y=126) cpasswordFrame.place(x=(17 if platform.system()=="Darwin" else 20),y=116)
cpasswordFrame.columnconfigure(0,weight=10) cpasswordFrame.columnconfigure(0,weight=10)
cpasswordFrame.grid_propagate(False) cpasswordFrame.grid_propagate(False)
# Confirm password input box # Confirm password input box
@ -262,7 +364,9 @@ cpasswordInput["state"] = "disabled"
# Start the encryption/decryption process # Start the encryption/decryption process
def start(): def start():
global inputFile,outputFile,password,ad,kept,working,gMode,headerRsc global inputFile,outputFile,password,ad,kept
global working,gMode,headerRsc,allFiles,files
global dragFolderPath
dummy.focus() dummy.focus()
reedsolo = False reedsolo = False
chunkSize = 2**20 chunkSize = 2**20
@ -277,7 +381,7 @@ def start():
mode = "decrypt" mode = "decrypt"
gMode = "decrypt" gMode = "decrypt"
# Check if Reed-Solomon was enabled by checking for "+" # Check if Reed-Solomon was enabled by checking for "+"
test = open(inputFile,"rb+") test = open(inputFile,"rb")
decider = test.read(1).decode("utf-8") decider = test.read(1).decode("utf-8")
test.close() test.close()
if decider=="+": if decider=="+":
@ -288,7 +392,7 @@ def start():
# Check if file already exists (getsize() throws error if file not found) # Check if file already exists (getsize() throws error if file not found)
try: try:
getsize(outputFile) getsize(outputFile)
force = messagebox.askyesno("Warning",overwriteNotice) force = messagebox.askyesno("Confirmation",overwriteNotice)
dummy.focus() dummy.focus()
if force!=1: if force!=1:
return return
@ -296,26 +400,11 @@ def start():
pass pass
# Disable inputs and buttons while encrypting/decrypting # Disable inputs and buttons while encrypting/decrypting
selectFileInput["state"] = "disabled" disableAllInputs()
passwordInput["state"] = "disabled"
cpasswordInput["state"] = "disabled"
adArea["state"] = "disabled"
startBtn["state"] = "disabled"
eraseBtn["state"] = "disabled"
keepBtn["state"] = "disabled"
rsBtn["state"] = "disabled"
# Make sure passwords match # Make sure passwords match
if passwordInput.get()!=cpasswordInput.get() and mode=="encrypt": if passwordInput.get()!=cpasswordInput.get() and mode=="encrypt":
selectFileInput["state"] = "normal" resetEncryptionUI()
passwordInput["state"] = "normal"
cpasswordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
eraseBtn["state"] = "normal"
rsBtn["state"] = "normal"
working = False
progress["value"] = 100
statusString.set("Passwords don't match.") statusString.set("Passwords don't match.")
return return
@ -329,6 +418,27 @@ def start():
# 13 bytes per 128 bytes, ~10% larger output file # 13 bytes per 128 bytes, ~10% larger output file
rsc = RSCodec(13) rsc = RSCodec(13)
# Compress files together if user dragged multiple files
if allFiles or files:
statusString.set(compressingNotice)
tmp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if files:
zfPath = Path(files[0]).parent.absolute()
else:
zfPath = Path(dirname(allFiles[0])).parent.absolute()
zfOffset = len(str(zfPath))
zfName = pathJoin(zfPath,tmp+".zip")
zf = ZipFile(zfName,"w")
for i in allFiles:
zf.write(i,i[zfOffset:])
for i in files:
zf.write(i,pathSplit(i)[1])
zf.close()
inputFile = zfName
outputFile = zfName+".pcv"
outputPath = dirname(outputFile)
# Set and get some variables # Set and get some variables
working = True working = True
headerBroken = False headerBroken = False
@ -340,7 +450,13 @@ def start():
wipe = erase.get()==1 wipe = erase.get()==1
# Open files # Open files
fin = open(inputFile,"rb+") try:
fin = open(inputFile,"rb")
except:
resetEncryptionUI()
statusString.set("Folder is empty.")
return
if reedsolo and mode=="decrypt": if reedsolo and mode=="decrypt":
# Move pointer one forward # Move pointer one forward
fin.read(1) fin.read(1)
@ -425,15 +541,7 @@ def start():
fout.close() fout.close()
remove(outputFile) remove(outputFile)
# Reset UI # Reset UI
selectFileInput["state"] = "normal" resetDecryptionUI()
passwordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
keepBtn["state"] = "normal"
working = False
progress.stop()
progress.config(mode="determinate")
progress["value"] = 100
return return
else: else:
kept = "badlyCorrupted" kept = "badlyCorrupted"
@ -472,14 +580,7 @@ def start():
fout.close() fout.close()
remove(outputFile) remove(outputFile)
# Reset UI # Reset UI
selectFileInput["state"] = "normal" resetDecryptionUI()
passwordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
keepBtn["state"] = "normal"
working = False
progress["value"] = 100
del key
return return
# Create XChaCha20-Poly1305 object # Create XChaCha20-Poly1305 object
@ -492,9 +593,6 @@ def start():
total = getsize(inputFile) total = getsize(inputFile)
# If secure wipe enabled, create a wiper object # If secure wipe enabled, create a wiper object
if wipe:
wiper = open(inputFile,"r+b")
wiper.seek(0)
# Keep track of time because it flies... # Keep track of time because it flies...
startTime = datetime.now() startTime = datetime.now()
@ -507,11 +605,7 @@ def start():
piece = fin.read(1104905) piece = fin.read(1104905)
else: else:
piece = fin.read(chunkSize) piece = fin.read(chunkSize)
if wipe:
# If securely wipe, write random trash
# to original file after reading it
trash = urandom(len(piece))
wiper.write(trash)
# If EOF # If EOF
if not piece: if not piece:
if mode=="encrypt": if mode=="encrypt":
@ -540,12 +634,7 @@ def start():
if keep.get()!=1: if keep.get()!=1:
remove(outputFile) remove(outputFile)
# Reset UI # Reset UI
selectFileInput["state"] = "normal" resetDecryptionUI()
passwordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
keepBtn["state"] = "normal"
working = False
del fin,fout,cipher,key del fin,fout,cipher,key
return return
else: else:
@ -566,12 +655,7 @@ def start():
if keep.get()!=1: if keep.get()!=1:
remove(outputFile) remove(outputFile)
# Reset UI # Reset UI
selectFileInput["state"] = "normal" resetDecryptionUI()
passwordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
keepBtn["state"] = "normal"
working = False
del fin,fout,cipher,key del fin,fout,cipher,key
return return
else: else:
@ -605,13 +689,7 @@ def start():
fout.close() fout.close()
remove(outputFile) remove(outputFile)
# Reset UI # Reset UI
selectFileInput["state"] = "normal" resetDecryptionUI()
passwordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
keepBtn["state"] = "normal"
working = False
progress["value"] = 100
del fin,fout,cipher,key del fin,fout,cipher,key
return return
else: else:
@ -639,60 +717,76 @@ def start():
data = cipher.decrypt(piece) data = cipher.decrypt(piece)
# Calculate speed, ETA, etc. # Calculate speed, ETA, etc.
first = False
elapsed = (datetime.now()-previousTime).total_seconds() or 0.0001 elapsed = (datetime.now()-previousTime).total_seconds() or 0.0001
sinceStart = (datetime.now()-startTime).total_seconds() or 0.0001 sinceStart = (datetime.now()-startTime).total_seconds() or 0.0001
previousTime = datetime.now() previousTime = datetime.now()
# Prevent divison by zero
if not elapsed:
elapsed = 0.1**6
percent = done*100/total percent = done*100/total
progress["value"] = percent progress["value"] = percent
rPercent = round(percent)
speed = (done/sinceStart)/10**6 speed = (done/sinceStart)/10**6 or 0.0001
# Prevent divison by zero
if not speed:
first = True
speed = 0.1**6
rSpeed = str(round(speed,2))
# Right-pad with zeros to large prevent layout shifts
while len(rSpeed.split(".")[1])!=2:
rSpeed += "0"
eta = round((total-done)/(speed*10**6)) eta = round((total-done)/(speed*10**6))
# Seconds to minutes if seconds more than 59 # Seconds to minutes if seconds more than 59
if eta>=60: if eta>=60:
# Set blank ETA if just starting
if sinceStart<0.5:
eta = "..."
else:
eta = f"{eta//60}m {eta%60}" eta = f"{eta//60}m {eta%60}"
if isinstance(eta,int) or isinstance(eta,float): if isinstance(eta,int) or isinstance(eta,float):
if eta<0: if eta<0:
eta = 0 eta = 0
# If it's the first round and no data/predictions yet...
if first:
statusString.set("...% at ... MB/s (ETA: ...s)")
else:
# Update status # Update status
info = f"{rPercent}% at {rSpeed} MB/s (ETA: {eta}s)" info = f"{percent:.0f}% at {speed:.2f} MB/s (ETA: {eta}s)"
if reedsolo and mode=="decrypt" and reedsoloFixedCount: if reedsolo and mode=="decrypt" and reedsoloFixedCount:
eng = "s" if reedsoloFixedCount!=1 else "" tmp = "s" if reedsoloFixedCount!=1 else ""
info += f", fixed {reedsoloFixedCount} corrupted byte{eng}" info += f", fixed {reedsoloFixedCount} corrupted byte{tmp}"
if reedsolo and mode=="decrypt" and reedsoloErrorCount: if reedsolo and mode=="decrypt" and reedsoloErrorCount:
info += f", {reedsoloErrorCount} MB unrecoverable" info += f", {reedsoloErrorCount} MB unrecoverable"
statusString.set(info) statusString.set(info)
# Increase done and write to output # Increase done and write to output
done += 1104905 if (reedsolo and mode=="decrypt") else chunkSize done += 1104905 if (reedsolo and mode=="decrypt") else chunkSize
fout.write(data) fout.write(data)
# Flush outputs, close files
if not kept:
fout.flush()
fsync(fout.fileno())
fout.close()
fin.close()
# Securely wipe files as necessary
if wipe:
if draggedFolderPaths:
for i in draggedFolderPaths:
secureWipe(i)
if files:
for i in range(len(files)):
statusString.set(erasingNotice+f" ({i}/{len(files)}")
progress["value"] = i/len(files)
secureWipe(files[i])
secureWipe(inputFile)
# Secure wipe not enabled
else:
if allFiles:
# Remove temporary zip file if created
remove(inputFile)
# Show appropriate notice if file corrupted or modified # Show appropriate notice if file corrupted or modified
if not kept: if not kept:
if mode=="encrypt": statusString.set(f"Completed. (Click here to show output)")
output = inputFile.split("/")[-1]+".pcv"
else:
output = inputFile.split("/")[-1].replace(".pcv","")
statusString.set(f"Completed. (Output: {output})")
# Show Reed-Solomon stats if it fixed corrupted bytes # Show Reed-Solomon stats if it fixed corrupted bytes
if mode=="decrypt" and reedsolo and reedsoloFixedCount: if mode=="decrypt" and reedsolo and reedsoloFixedCount:
statusString.set(f"Completed with {reedsoloFixedCount} bytes fixed."+ statusString.set(
f" (Output: {output})") f"Completed with {reedsoloFixedCount}"+
f" bytes fixed. (Output: {output})"
)
else: else:
if kept=="modified": if kept=="modified":
statusString.set(kModifiedNotice) statusString.set(kModifiedNotice)
@ -701,45 +795,32 @@ def start():
else: else:
statusString.set(kVeryCorruptedNotice) statusString.set(kVeryCorruptedNotice)
status.config(cursor="hand2")
# A little hack since strings are immutable
output = "".join([i for i in outputFile])
# Bind the output file
if platform.system()=="Windows":
status.bind("<Button-1>",
lambda e:showOutput(output.replace("/","\\"))
)
else:
status.bind("<Button-1>",
lambda e:showOutput(output)
)
# Reset variables and UI states # Reset variables and UI states
selectFileInput["state"] = "normal" resetUI()
adArea["state"] = "normal" status["state"] = "normal"
adArea.delete("1.0",tkinter.END)
adArea["state"] = "disabled"
startBtn["state"] = "disabled"
passwordInput["state"] = "normal"
passwordInput.delete(0,"end")
passwordInput["state"] = "disabled"
cpasswordInput["state"] = "normal"
cpasswordInput.delete(0,"end")
cpasswordInput["state"] = "disabled"
progress["value"] = 0
inputString.set("Please select a file.")
keepBtn["state"] = "normal"
keep.set(0)
keepBtn["state"] = "disabled"
eraseBtn["state"] = "normal"
erase.set(0)
eraseBtn["state"] = "disabled"
rs.set(0)
rsBtn["state"] = "disabled"
if not kept:
fout.flush()
fsync(fout.fileno())
fout.close()
fin.close()
if wipe:
# Make sure to flush file
wiper.flush()
fsync(wiper.fileno())
wiper.close()
remove(inputFile)
inputFile = "" inputFile = ""
outputFile = "" outputFile = ""
password = "" password = ""
ad = "" ad = ""
kept = False kept = False
working = False working = False
allFiles = False
dragFolderPath = False
# Wipe keys for safety # Wipe keys for safety
del fin,fout,cipher,key del fin,fout,cipher,key
@ -751,34 +832,124 @@ def wrapper():
start() start()
except: except:
# Reset UI accordingly # Reset UI accordingly
progress.stop()
progress.config(mode="determinate")
progress["value"] = 100
selectFileInput["state"] = "normal"
passwordInput["state"] = "normal"
startBtn["state"] = "normal"
if gMode=="decrypt": if gMode=="decrypt":
keepBtn["state"] = "normal" resetDecryptionUI()
else: else:
adArea["state"] = "normal" resetEncryptionUI()
cpasswordInput["state"] = "normal"
rsBtn["state"] = "normal"
eraseBtn["state"] = "normal"
statusString.set(unknownErrorNotice) statusString.set(unknownErrorNotice)
dummy.focus() dummy.focus()
working = False
finally: finally:
sys.exit(0) sys.exit(0)
# Encryption/decrypt is done is a separate thread # Encryption/decrypt is done is a separate thread so the UI
# so the UI isn't blocked. This is a wrapper # isn't blocked. This is a wrapper to spawn a thread and start it.
# to spawn a thread and start it.
def startWorker(): def startWorker():
thread = Thread(target=wrapper,daemon=True) thread = Thread(target=wrapper,daemon=True)
thread.start() thread.start()
# Securely wipe file
def secureWipe(fin):
statusString.set(erasingNotice)
# Check platform, erase accordingly
if platform.system()=="Windows":
if isdir(fin):
paths = []
for i in Path(fin).rglob("*"):
if dirname(i) not in paths:
paths.append(dirname(i))
for i in range(len(paths)):
statusString.set(erasingNotice+f" ({i}/{len(paths)})")
progress["value"] = 100*i/len(paths)
system(f'cd "{paths[i]}" && "{rootDir}/sdelete64.exe" * -p 4 -s -nobanner')
system(f'cd "{rootDir}"')
rmtree(fin)
else:
statusString.set(erasingNotice)
progress["value"] = 100
system(f'sdelete64.exe "{fin}" -p 4 -nobanner')
elif platform.system()=="Darwin":
system(f'rm -rfP "{fin}"')
else:
system(f'shred -uz "{fin}" -n 4')
# Disable all inputs while encrypting/decrypting
def disableAllInputs():
passwordInput["state"] = "disabled"
cpasswordInput["state"] = "disabled"
adArea["state"] = "disabled"
startBtn["state"] = "disabled"
eraseBtn["state"] = "disabled"
keepBtn["state"] = "disabled"
rsBtn["state"] = "disabled"
# Reset UI to encryption state
def resetEncryptionUI():
global working
passwordInput["state"] = "normal"
cpasswordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
eraseBtn["state"] = "normal"
rsBtn["state"] = "normal"
working = False
progress.stop()
progress.config(mode="determinate")
progress["value"] = 100
# Reset UI to decryption state
def resetDecryptionUI():
global working
passwordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
keepBtn["state"] = "normal"
working = False
progress.stop()
progress.config(mode="determinate")
progress["value"] = 100
# Reset UI to original state (no file selected)
def resetUI():
adArea["state"] = "normal"
adArea.delete("1.0",tkinter.END)
adArea["state"] = "disabled"
adLabel["state"] = "disabled"
startBtn["state"] = "disabled"
passwordInput["state"] = "normal"
passwordInput.delete(0,"end")
passwordInput["state"] = "disabled"
passwordLabel["state"] = "disabled"
cpasswordInput["state"] = "normal"
cpasswordInput.delete(0,"end")
cpasswordInput["state"] = "disabled"
cpasswordString.set("Confirm password:")
cpasswordLabel["state"] = "disabled"
status["state"] = "disabled"
progress["value"] = 0
inputString.set("Drag and drop file(s) and folder(s) into this window.")
keepBtn["state"] = "normal"
keep.set(0)
keepBtn["state"] = "disabled"
eraseBtn["state"] = "normal"
erase.set(0)
eraseBtn["state"] = "disabled"
rs.set(0)
rsBtn["state"] = "disabled"
progress.stop()
progress.config(mode="determinate")
progress["value"] = 0
def showOutput(file):
if platform.system()=="Windows":
system(f'explorer /select,"{file}"')
elif platform.system()=="Darwin":
system(f'cd "{dirname(file)}"; open -R {pathSplit(file)[1]}')
system(f'cd "{rootDir}"')
else:
system(f'xdg-open "{dirname(file)}"')
# ad stands for "associated data"/metadata # ad stands for "associated data"/metadata
adLabelString = tkinter.StringVar(tk) adLabelString = tkinter.StringVar(tk)
adLabelString.set(adString) adLabelString.set(adString)
@ -786,8 +957,9 @@ adLabel = tkinter.ttk.Label(
tk, tk,
textvariable=adLabelString textvariable=adLabelString
) )
adLabel.place(x=17,y=158) adLabel.place(x=17,y=148)
adLabel.config(background="#f5f6f7") adLabel.config(background="#ffffff")
adLabel["state"] = "disabled"
# Frame so metadata text box can fill width # Frame so metadata text box can fill width
adFrame = tkinter.Frame( adFrame = tkinter.Frame(
@ -795,7 +967,7 @@ adFrame = tkinter.Frame(
width=440, width=440,
height=100 height=100
) )
adFrame.place(x=20,y=178) adFrame.place(x=20,y=168)
adFrame.columnconfigure(0,weight=10) adFrame.columnconfigure(0,weight=10)
adFrame.grid_propagate(False) adFrame.grid_propagate(False)
@ -818,7 +990,7 @@ keepBtn = tkinter.ttk.Checkbutton(
offvalue=0, offvalue=0,
command=lambda:dummy.focus() command=lambda:dummy.focus()
) )
keepBtn.place(x=18,y=290) keepBtn.place(x=18,y=280)
keepBtn["state"] = "disabled" keepBtn["state"] = "disabled"
# Check box for securely erasing original file # Check box for securely erasing original file
@ -831,7 +1003,7 @@ eraseBtn = tkinter.ttk.Checkbutton(
offvalue=0, offvalue=0,
command=lambda:dummy.focus() command=lambda:dummy.focus()
) )
eraseBtn.place(x=18,y=310) eraseBtn.place(x=18,y=300)
eraseBtn["state"] = "disabled" eraseBtn["state"] = "disabled"
# Check box for Reed Solomon # Check box for Reed Solomon
@ -844,16 +1016,16 @@ rsBtn = tkinter.ttk.Checkbutton(
offvalue=0, offvalue=0,
command=lambda:dummy.focus() command=lambda:dummy.focus()
) )
rsBtn.place(x=18,y=330) rsBtn.place(x=18,y=320)
rsBtn["state"] = "disabled" rsBtn["state"] = "disabled"
# Frame so start button can fill width # Frame so start button can fill width
startFrame = tkinter.Frame( startFrame = tkinter.Frame(
tk, tk,
width=442, width=442,
height=25 height=24
) )
startFrame.place(x=19,y=360) startFrame.place(x=19,y=350)
startFrame.columnconfigure(0,weight=10) startFrame.columnconfigure(0,weight=10)
startFrame.grid_propagate(False) startFrame.grid_propagate(False)
# Start button # Start button
@ -872,7 +1044,7 @@ progress = tkinter.ttk.Progressbar(
length=440, length=440,
mode="determinate" mode="determinate"
) )
progress.place(x=20,y=388) progress.place(x=20,y=378)
# Status label # Status label
statusString = tkinter.StringVar(tk) statusString = tkinter.StringVar(tk)
@ -881,8 +1053,9 @@ status = tkinter.ttk.Label(
tk, tk,
textvariable=statusString textvariable=statusString
) )
status.place(x=17,y=416) status.place(x=17,y=406)
status.config(background="#f5f6f7") status.config(background="#ffffff")
status["state"] = "disabled"
# Credits :) # Credits :)
hint = "Created by Evan Su. Click for details and source." hint = "Created by Evan Su. Click for details and source."
@ -894,21 +1067,21 @@ credits = tkinter.ttk.Label(
cursor="hand2" cursor="hand2"
) )
credits["state"] = "disabled" credits["state"] = "disabled"
credits.config(background="#f5f6f7") credits.config(background="#ffffff")
credits.place(x=17,y=446) credits.place(x=17,y=436)
source = "https://github.com/HACKERALERT/Picocrypt" source = "https://github.com/HACKERALERT/Picocrypt"
credits.bind("<Button-1>",lambda e:webbrowser.open(source)) credits.bind("<Button-1>",lambda e:webbrowser.open(source))
# Version # Version
versionString = tkinter.StringVar(tk) versionString = tkinter.StringVar(tk)
versionString.set("v1.10") versionString.set("v1.11")
version = tkinter.ttk.Label( version = tkinter.ttk.Label(
tk, tk,
textvariable=versionString textvariable=versionString
) )
version["state"] = "disabled" version["state"] = "disabled"
version.config(background="#f5f6f7") version.config(background="#ffffff")
version.place(x=430,y=446) version.place(x=(420 if platform.system()=="Darwin" else 430),y=436)
# Dummy button to remove focus from other buttons # Dummy button to remove focus from other buttons
# and prevent ugly border highlighting # and prevent ugly border highlighting
@ -923,16 +1096,28 @@ def createRsc():
headerRsc = RSCodec(128) headerRsc = RSCodec(128)
sys.exit(0) sys.exit(0)
def prepare():
if platform.system()=="Windows":
system("sdelete64.exe /accepteula")
# Close window only if not encrypting or decrypting # Close window only if not encrypting or decrypting
def onClose(): def onClose():
global outputFile
if not working: if not working:
tk.destroy() tk.destroy()
else:
force = messagebox.askyesno("Confirmation",cancelNotice)
if force:
tk.destroy()
# Main application loop # Main application loop
if __name__=="__main__": if __name__=="__main__":
# Create Reed-Solomon header codec # Create Reed-Solomon header codec
tmp = Thread(target=createRsc,daemon=True) tmp = Thread(target=createRsc,daemon=True)
tmp.start() tmp.start()
# Prepare application
tmp = Thread(target=prepare,daemon=True)
tmp.start()
# Start tkinter # Start tkinter
tk.protocol("WM_DELETE_WINDOW",onClose) tk.protocol("WM_DELETE_WINDOW",onClose)
tk.mainloop() tk.mainloop()