2021-02-19 16:30:39 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2021-03-15 20:08:47 +01:00
|
|
|
"""
|
|
|
|
Dependencies: argon2-cffi, pycryptodome, reedsolo
|
|
|
|
Copyright (c) Evan Su (https://evansu.cc)
|
2021-03-16 17:10:54 +01:00
|
|
|
Released under a GNU GPL v3 License
|
2021-03-15 20:08:47 +01:00
|
|
|
https://github.com/HACKERALERT/Picocrypt
|
|
|
|
"""
|
2021-02-19 16:30:39 +01:00
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Test if libraries are installed
|
2021-02-19 16:30:39 +01:00
|
|
|
try:
|
2021-03-13 18:11:06 +01:00
|
|
|
from argon2.low_level import hash_secret_raw
|
|
|
|
from Crypto.Cipher import ChaCha20_Poly1305
|
2021-03-15 19:41:15 +01:00
|
|
|
try:
|
|
|
|
from creedsolo import ReedSolomonError
|
|
|
|
except:
|
|
|
|
from reedsolo import ReedSolomonError
|
2021-02-19 16:30:39 +01:00
|
|
|
except:
|
2021-03-13 18:11:06 +01:00
|
|
|
# Libraries missing, install them
|
|
|
|
from os import system
|
2021-03-15 19:41:15 +01:00
|
|
|
try:
|
|
|
|
# Debian/Ubuntu based
|
|
|
|
system("sudo apt-get install python3-tk")
|
|
|
|
except:
|
|
|
|
# Fedora
|
|
|
|
system("sudo dnf install python3-tkinter")
|
2021-03-16 17:10:54 +01:00
|
|
|
|
2021-03-15 19:41:15 +01:00
|
|
|
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")
|
2021-02-19 16:30:39 +01:00
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Imports
|
2021-02-21 17:48:15 +01:00
|
|
|
from tkinter import filedialog,messagebox
|
2021-02-19 01:43:18 +01:00
|
|
|
from threading import Thread
|
|
|
|
from datetime import datetime
|
|
|
|
from argon2.low_level import hash_secret_raw,Type
|
|
|
|
from Crypto.Cipher import ChaCha20_Poly1305
|
|
|
|
from hashlib import sha3_512
|
|
|
|
from secrets import compare_digest
|
|
|
|
from os import urandom,fsync,remove
|
|
|
|
from os.path import getsize,expanduser
|
|
|
|
import sys
|
|
|
|
import tkinter
|
|
|
|
import tkinter.ttk
|
2021-02-24 17:54:34 +01:00
|
|
|
import tkinter.scrolledtext
|
2021-02-19 01:43:18 +01:00
|
|
|
import webbrowser
|
2021-03-15 19:41:15 +01:00
|
|
|
try:
|
|
|
|
from creedsolo import RSCodec,ReedSolomonError
|
|
|
|
except:
|
|
|
|
from reedsolo import RSCodec,ReedSolomonError
|
2021-02-19 01:43:18 +01:00
|
|
|
|
2021-03-16 17:10:54 +01:00
|
|
|
# Tk/Tcl is a little barbaric, so I'm disabling
|
|
|
|
# high DPI so it doesn't scale bad and look horrible
|
2021-03-13 18:20:29 +01:00
|
|
|
try:
|
|
|
|
from ctypes import windll
|
2021-03-13 18:56:27 +01:00
|
|
|
windll.shcore.SetProcessDpiAwareness(0)
|
2021-03-13 18:20:29 +01:00
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Global variables and notices
|
2021-02-19 01:43:18 +01:00
|
|
|
inputFile = ""
|
|
|
|
outputFile = ""
|
|
|
|
password = ""
|
|
|
|
ad = ""
|
2021-03-13 18:11:06 +01:00
|
|
|
kept = False
|
|
|
|
working = False
|
2021-02-19 01:43:18 +01:00
|
|
|
adString = "File metadata (used to store some text along with the file):"
|
|
|
|
passwordNotice = "Error. The provided password is incorrect."
|
|
|
|
corruptedNotice = "Error. The input file is corrupted."
|
|
|
|
modifiedNotice = "Error. The input file has been intentionally modified."
|
|
|
|
kCorruptedNotice = "The input file is corrupted, but the output has been kept."
|
|
|
|
kModifiedNotice = "The input file has been intentionally modified, but the output has been kept."
|
2021-02-25 17:35:54 +01:00
|
|
|
derivingNotice = "Deriving key (takes a few seconds)..."
|
2021-02-19 01:43:18 +01:00
|
|
|
keepNotice = "Keep decrypted output even if it's corrupted or modified"
|
|
|
|
eraseNotice = "Securely erase and delete original file"
|
2021-02-21 17:48:15 +01:00
|
|
|
overwriteNotice = "Output file already exists. Would you like to overwrite it?"
|
2021-03-15 19:41:15 +01:00
|
|
|
rsNotice = "Prevent corruption using Reed-Solomon"
|
|
|
|
rscNotice = "Creating Reed-Solomon tables..."
|
2021-02-25 17:35:54 +01:00
|
|
|
unknownErrorNotice = "Unknown error occured. Please try again."
|
2021-02-19 01:43:18 +01:00
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Create root Tk
|
2021-02-19 01:43:18 +01:00
|
|
|
tk = tkinter.Tk()
|
2021-03-15 19:41:15 +01:00
|
|
|
tk.geometry("480x440")
|
2021-02-19 01:43:18 +01:00
|
|
|
tk.title("Picocrypt")
|
|
|
|
tk.configure(background="#f5f6f7")
|
|
|
|
tk.resizable(0,0)
|
|
|
|
|
2021-03-16 17:10:54 +01:00
|
|
|
# Try setting window icon if included with Picocrypt
|
2021-03-13 18:11:06 +01:00
|
|
|
try:
|
|
|
|
favicon = tkinter.PhotoImage(file="./key.png")
|
|
|
|
tk.iconphoto(False,favicon)
|
|
|
|
except:
|
|
|
|
pass
|
2021-02-19 01:43:18 +01:00
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Some styling
|
2021-02-19 01:43:18 +01:00
|
|
|
s = tkinter.ttk.Style()
|
|
|
|
s.configure("TCheckbutton",background="#f5f6f7")
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Event when user selects an input file
|
2021-02-19 01:43:18 +01:00
|
|
|
def inputSelected():
|
2021-03-13 18:11:06 +01:00
|
|
|
global inputFile,working
|
|
|
|
dummy.focus()
|
|
|
|
|
|
|
|
# Try to handle when select file is cancelled
|
|
|
|
try:
|
|
|
|
# Ask for input file
|
|
|
|
suffix = ""
|
|
|
|
tmp = filedialog.askopenfilename(
|
|
|
|
initialdir=expanduser("~")
|
|
|
|
)
|
|
|
|
if len(tmp)==0:
|
|
|
|
# Exception will be caught by except below
|
|
|
|
raise Exception("No file selected.")
|
|
|
|
inputFile = tmp
|
2021-03-16 20:07:25 +01:00
|
|
|
# Decide if encrypting or decrypting (".pcf" is the legacy Picocrypt extension,
|
|
|
|
# ".pcv" is the newer Picocrypt extension. Both are cross-compatible, but
|
|
|
|
# I just think ".pcv" is better because it stands for "Picocrypt Volume")
|
|
|
|
if ".pcf" in inputFile.split("/")[-1] or ".pcv" in inputFile.split("/")[-1]:
|
2021-03-15 19:41:15 +01:00
|
|
|
suffix = " (will decrypt)"
|
2021-03-13 18:11:06 +01:00
|
|
|
fin = open(inputFile,"rb+")
|
|
|
|
# Read file metadata
|
|
|
|
adlen = b""
|
|
|
|
while True:
|
|
|
|
letter = fin.read(1)
|
2021-03-15 19:41:15 +01:00
|
|
|
if letter!=b"+":
|
|
|
|
adlen += letter
|
2021-03-13 18:11:06 +01:00
|
|
|
if letter==b"|":
|
|
|
|
adlen = adlen[:-1]
|
|
|
|
break
|
|
|
|
ad = fin.read(int(adlen.decode("utf-8")))
|
|
|
|
fin.close()
|
|
|
|
# Insert the metadata into its text box
|
|
|
|
adArea["state"] = "normal"
|
|
|
|
adArea.delete("1.0",tkinter.END)
|
|
|
|
adArea.insert("1.0",ad.decode("utf-8"))
|
|
|
|
adArea["state"] = "disabled"
|
|
|
|
adLabelString.set("File metadata (read only):")
|
|
|
|
keepBtn["state"] = "normal"
|
|
|
|
eraseBtn["state"] = "disabled"
|
2021-03-15 19:41:15 +01:00
|
|
|
rsBtn["state"] = "disabled"
|
2021-03-13 18:11:06 +01:00
|
|
|
else:
|
|
|
|
# Update the UI
|
|
|
|
eraseBtn["state"] = "normal"
|
|
|
|
keepBtn["state"] = "disabled"
|
2021-03-15 19:41:15 +01:00
|
|
|
rsBtn["state"] = "normal"
|
2021-03-13 18:11:06 +01:00
|
|
|
adArea["state"] = "normal"
|
|
|
|
adArea.delete("1.0",tkinter.END)
|
2021-03-15 19:41:15 +01:00
|
|
|
suffix = " (will encrypt)"
|
2021-03-13 18:11:06 +01:00
|
|
|
adLabelString.set(adString)
|
|
|
|
# Enable password box, etc.
|
|
|
|
inputString.set(inputFile.split("/")[-1]+suffix)
|
|
|
|
passwordInput["state"] = "normal"
|
|
|
|
passwordInput.delete(0,"end")
|
|
|
|
startBtn["state"] = "normal"
|
|
|
|
statusString.set("Ready.")
|
|
|
|
progress["value"] = 0
|
|
|
|
# File decode error
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
passwordInput["state"] = "normal"
|
|
|
|
passwordInput.delete(0,"end")
|
|
|
|
statusString.set(corruptedNotice)
|
|
|
|
# No file selected, do nothing
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
# Focus the dummy button to remove ugly borders
|
|
|
|
finally:
|
|
|
|
dummy.focus()
|
|
|
|
working = False
|
|
|
|
|
|
|
|
# Button to select input file
|
2021-02-19 01:43:18 +01:00
|
|
|
selectFileInput = tkinter.ttk.Button(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
text="Select file",
|
|
|
|
command=inputSelected,
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
selectFileInput.place(x=19,y=20)
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Label that displays selected input file
|
2021-02-19 01:43:18 +01:00
|
|
|
inputString = tkinter.StringVar(tk)
|
|
|
|
inputString.set("Please select a file.")
|
|
|
|
selectedInput = tkinter.ttk.Label(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
textvariable=inputString
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
selectedInput.config(background="#f5f6f7")
|
|
|
|
selectedInput.place(x=104,y=23)
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Label that prompts user to enter a password
|
2021-02-19 01:43:18 +01:00
|
|
|
passwordString = tkinter.StringVar(tk)
|
|
|
|
passwordString.set("Password:")
|
|
|
|
passwordLabel = tkinter.ttk.Label(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
textvariable=passwordString
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
passwordLabel.place(x=17,y=56)
|
|
|
|
passwordLabel.config(background="#f5f6f7")
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# A frame to make password input fill width
|
2021-02-19 01:43:18 +01:00
|
|
|
passwordFrame = tkinter.Frame(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
width=440,
|
|
|
|
height=22
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
passwordFrame.place(x=20,y=76)
|
|
|
|
passwordFrame.columnconfigure(0,weight=10)
|
|
|
|
passwordFrame.grid_propagate(False)
|
2021-03-13 18:11:06 +01:00
|
|
|
# Password input box
|
2021-02-19 01:43:18 +01:00
|
|
|
passwordInput = tkinter.ttk.Entry(
|
2021-03-13 18:11:06 +01:00
|
|
|
passwordFrame
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
passwordInput.grid(sticky="nesw")
|
|
|
|
passwordInput["state"] = "disabled"
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Start the encryption/decryption process
|
2021-02-19 01:43:18 +01:00
|
|
|
def start():
|
2021-03-13 18:11:06 +01:00
|
|
|
global inputFile,outputFile,password,ad,kept,working
|
2021-03-15 19:41:15 +01:00
|
|
|
dummy.focus()
|
|
|
|
reedsolo = False
|
|
|
|
chunkSize = 2**20
|
2021-03-13 18:11:06 +01:00
|
|
|
|
|
|
|
# Decide if encrypting or decrypting
|
2021-03-16 20:07:25 +01:00
|
|
|
if ".pcf" not in inputFile and ".pcv" not in inputFile:
|
2021-03-13 18:11:06 +01:00
|
|
|
mode = "encrypt"
|
2021-03-16 20:07:25 +01:00
|
|
|
outputFile = inputFile+".pcv"
|
2021-03-15 19:41:15 +01:00
|
|
|
reedsolo = rs.get()==1
|
2021-03-13 18:11:06 +01:00
|
|
|
else:
|
|
|
|
mode = "decrypt"
|
2021-03-16 17:10:54 +01:00
|
|
|
# Check if Reed-Solomon was enabled by checking for "+"
|
2021-03-15 19:41:15 +01:00
|
|
|
test = open(inputFile,"rb+")
|
|
|
|
decider = test.read(1).decode("utf-8")
|
|
|
|
test.close()
|
|
|
|
if decider=="+":
|
|
|
|
reedsolo = True
|
2021-03-16 17:10:54 +01:00
|
|
|
# Decrypted output is just input file without the extension
|
2021-03-13 18:11:06 +01:00
|
|
|
outputFile = inputFile[:-4]
|
|
|
|
|
2021-03-16 17:10:54 +01:00
|
|
|
# Check if file already exists (getsize() throws error if file not found)
|
2021-03-13 18:11:06 +01:00
|
|
|
try:
|
|
|
|
getsize(outputFile)
|
|
|
|
force = messagebox.askyesno("Warning",overwriteNotice)
|
|
|
|
dummy.focus()
|
|
|
|
if force!=1:
|
|
|
|
return
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2021-03-15 19:41:15 +01:00
|
|
|
# Set progress bar indeterminate
|
|
|
|
progress.config(mode="indeterminate")
|
|
|
|
progress.start(15)
|
|
|
|
|
|
|
|
# Create Reed-Solomon object
|
|
|
|
if reedsolo:
|
|
|
|
statusString.set(rscNotice)
|
2021-03-16 17:10:54 +01:00
|
|
|
# 8 bytes per 128 bytes, 6.25% larger output file
|
2021-03-15 19:41:15 +01:00
|
|
|
rsc = RSCodec(8)
|
|
|
|
reedsoloFixedCount = 0
|
|
|
|
reedsoloErrorCount = 0
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Set and get some variables
|
|
|
|
working = True
|
|
|
|
dummy.focus()
|
|
|
|
password = passwordInput.get().encode("utf-8")
|
|
|
|
ad = adArea.get("1.0",tkinter.END).encode("utf-8")
|
|
|
|
wipe = erase.get()==1
|
|
|
|
|
2021-03-16 17:10:54 +01:00
|
|
|
# Disable inputs and buttons while encrypting/decrypting
|
2021-03-13 18:11:06 +01:00
|
|
|
selectFileInput["state"] = "disabled"
|
|
|
|
passwordInput["state"] = "disabled"
|
|
|
|
adArea["state"] = "disabled"
|
|
|
|
startBtn["state"] = "disabled"
|
2021-03-15 19:41:15 +01:00
|
|
|
eraseBtn["state"] = "disabled"
|
2021-03-13 18:11:06 +01:00
|
|
|
keepBtn["state"] = "disabled"
|
2021-03-15 19:41:15 +01:00
|
|
|
rsBtn["state"] = "disabled"
|
2021-03-13 18:11:06 +01:00
|
|
|
|
2021-03-16 17:10:54 +01:00
|
|
|
# Open files
|
2021-03-13 18:11:06 +01:00
|
|
|
fin = open(inputFile,"rb+")
|
2021-03-15 19:41:15 +01:00
|
|
|
if reedsolo and mode=="decrypt":
|
2021-03-16 17:10:54 +01:00
|
|
|
# Move pointer one forward
|
2021-03-15 19:41:15 +01:00
|
|
|
fin.read(1)
|
2021-03-13 18:11:06 +01:00
|
|
|
fout = open(outputFile,"wb+")
|
2021-03-15 19:41:15 +01:00
|
|
|
if reedsolo and mode=="encrypt":
|
2021-03-16 17:10:54 +01:00
|
|
|
# Signal that Reed-Solomon was enabled with "+"
|
2021-03-15 19:41:15 +01:00
|
|
|
fout.write(b"+")
|
2021-03-13 18:11:06 +01:00
|
|
|
|
|
|
|
# Generate values for encryption if encrypting
|
|
|
|
if mode=="encrypt":
|
|
|
|
salt = urandom(16)
|
|
|
|
nonce = urandom(24)
|
2021-03-16 17:10:54 +01:00
|
|
|
fout.write(str(len(ad)).encode("utf-8")) # Length of metadata
|
|
|
|
fout.write(b"|") # Separator
|
2021-03-16 17:34:59 +01:00
|
|
|
fout.write(ad) # Metadata (associated data)
|
2021-03-16 20:07:25 +01:00
|
|
|
|
2021-03-16 17:10:54 +01:00
|
|
|
# Write zeros as placeholder, come back to write over it later
|
2021-03-16 20:07:25 +01:00
|
|
|
# Note that 8 additional bytes are added if Reed-Solomon is enabled
|
|
|
|
fout.write(b"0"*(64+(8 if reedsolo else 0))) # SHA3-512 of encryption key
|
|
|
|
fout.write(b"0"*(64+(8 if reedsolo else 0))) # CRC of file
|
|
|
|
fout.write(b"0"*(16+(8 if reedsolo else 0))) # Poly1305 tag
|
|
|
|
# If Reed-Solomon is enabled, encode the salt and nonce, otherwise write them raw
|
|
|
|
fout.write(bytes(rsc.encode(salt)) if reedsolo else salt) # Argon2 salt
|
|
|
|
fout.write(bytes(rsc.encode(nonce)) if reedsolo else nonce) # ChaCha20 nonce
|
2021-03-13 18:11:06 +01:00
|
|
|
# If decrypting, read values from file
|
|
|
|
else:
|
|
|
|
# Read past metadata into actual data
|
2021-03-13 18:18:01 +01:00
|
|
|
adlen = b""
|
2021-03-13 18:11:06 +01:00
|
|
|
while True:
|
|
|
|
letter = fin.read(1)
|
2021-03-13 18:18:01 +01:00
|
|
|
adlen += letter
|
2021-03-13 18:11:06 +01:00
|
|
|
if letter==b"|":
|
2021-03-13 18:18:01 +01:00
|
|
|
adlen = adlen[:-1]
|
2021-03-13 18:11:06 +01:00
|
|
|
break
|
|
|
|
fin.read(int(adlen.decode("utf-8")))
|
2021-03-16 17:10:54 +01:00
|
|
|
# Read the salt, nonce, etc.
|
2021-03-16 20:07:25 +01:00
|
|
|
# Read 8 extra bytes if Reed-Solomon is enabled
|
|
|
|
cs = fin.read(72 if reedsolo else 64)
|
|
|
|
crccs = fin.read(72 if reedsolo else 64)
|
|
|
|
digest = fin.read(24 if reedsolo else 16)
|
|
|
|
salt = fin.read(24 if reedsolo else 16)
|
|
|
|
nonce = fin.read(32 if reedsolo else 24)
|
|
|
|
# If Reed-Solomon is enabled, decode each value
|
|
|
|
if reedsolo:
|
|
|
|
cs = bytes(rsc.decode(cs)[0])
|
|
|
|
crccs = bytes(rsc.decode(crccs)[0])
|
|
|
|
digest = bytes(rsc.decode(digest)[0])
|
|
|
|
salt = bytes(rsc.decode(salt)[0])
|
|
|
|
nonce = bytes(rsc.decode(nonce)[0])
|
2021-03-13 18:11:06 +01:00
|
|
|
|
2021-03-15 19:41:15 +01:00
|
|
|
# Show notice about key derivation
|
2021-03-13 18:11:06 +01:00
|
|
|
statusString.set(derivingNotice)
|
|
|
|
|
|
|
|
# Derive argon2id key
|
|
|
|
key = hash_secret_raw(
|
|
|
|
password,
|
|
|
|
salt,
|
|
|
|
time_cost=8, # 8 iterations
|
2021-03-15 19:41:15 +01:00
|
|
|
memory_cost=2**20, # 2^20 Kibibytes (1GiB)
|
2021-03-13 18:11:06 +01:00
|
|
|
parallelism=8, # 8 parallel threads
|
|
|
|
hash_len=32,
|
|
|
|
type=Type.ID
|
|
|
|
)
|
|
|
|
|
|
|
|
# Key deriving done, set progress bar determinate
|
|
|
|
progress.stop()
|
|
|
|
progress.config(mode="determinate")
|
|
|
|
progress["value"] = 0
|
|
|
|
|
|
|
|
# Compute hash of derived key
|
|
|
|
check = sha3_512(key).digest()
|
|
|
|
|
|
|
|
# If decrypting, check if key is correct
|
|
|
|
if mode=="decrypt":
|
|
|
|
# If key is incorrect...
|
|
|
|
if not compare_digest(check,cs):
|
|
|
|
statusString.set(passwordNotice)
|
|
|
|
fin.close()
|
|
|
|
fout.close()
|
|
|
|
remove(outputFile)
|
2021-03-16 17:10:54 +01:00
|
|
|
# Reset UI
|
2021-03-13 18:11:06 +01:00
|
|
|
selectFileInput["state"] = "normal"
|
|
|
|
passwordInput["state"] = "normal"
|
|
|
|
adArea["state"] = "normal"
|
|
|
|
startBtn["state"] = "normal"
|
|
|
|
keepBtn["state"] = "normal"
|
|
|
|
working = False
|
|
|
|
del key
|
|
|
|
return
|
|
|
|
|
|
|
|
# Create XChaCha20-Poly1305 object
|
|
|
|
cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce)
|
|
|
|
# Cyclic redundancy check for file corruption
|
|
|
|
crc = sha3_512()
|
|
|
|
|
2021-03-16 17:10:54 +01:00
|
|
|
# Amount of data encrypted/decrypted, total file size, starting time
|
2021-03-13 18:11:06 +01:00
|
|
|
done = 0
|
|
|
|
total = getsize(inputFile)
|
|
|
|
startTime = datetime.now()
|
|
|
|
|
|
|
|
# If secure wipe enabled, create a wiper object
|
|
|
|
if wipe:
|
|
|
|
wiper = open(inputFile,"r+b")
|
|
|
|
wiper.seek(0)
|
|
|
|
|
|
|
|
# Continously read file in chunks of 1MB
|
|
|
|
while True:
|
2021-03-15 19:41:15 +01:00
|
|
|
if mode=="decrypt" and reedsolo:
|
2021-03-16 17:10:54 +01:00
|
|
|
# Read a chunk plus Reed-Solomon recovery bytes
|
2021-03-15 19:41:15 +01:00
|
|
|
piece = fin.read(1082544)
|
|
|
|
else:
|
|
|
|
piece = fin.read(chunkSize)
|
2021-03-13 18:11:06 +01:00
|
|
|
if wipe:
|
|
|
|
# If securely wipe, write random trash
|
|
|
|
# to original file after reading it
|
|
|
|
trash = urandom(len(piece))
|
|
|
|
wiper.write(trash)
|
|
|
|
# If EOF
|
|
|
|
if not piece:
|
|
|
|
if mode=="encrypt":
|
2021-03-16 17:10:54 +01:00
|
|
|
# Get the cipher MAC tag (Poly1305)
|
2021-03-13 18:11:06 +01:00
|
|
|
digest = cipher.digest()
|
|
|
|
fout.flush()
|
|
|
|
fout.close()
|
|
|
|
fout = open(outputFile,"r+b")
|
2021-03-16 17:10:54 +01:00
|
|
|
# Compute the offset and seek to it
|
2021-03-15 19:41:15 +01:00
|
|
|
rsOffset = 1 if reedsolo else 0
|
|
|
|
fout.seek(len(str(len(ad)))+1+len(ad)+rsOffset)
|
2021-03-16 17:10:54 +01:00
|
|
|
# Write hash of key, CRC, and Poly1305 MAC tag
|
2021-03-16 20:07:25 +01:00
|
|
|
# Reed-Solomon-encode if selected by user
|
|
|
|
if reedsolo:
|
|
|
|
fout.write(bytes(rsc.encode(check)))
|
|
|
|
fout.write(bytes(rsc.encode(crc.digest())))
|
|
|
|
fout.write(bytes(rsc.encode(digest)))
|
|
|
|
else:
|
|
|
|
fout.write(check)
|
|
|
|
fout.write(crc.digest())
|
|
|
|
fout.write(digest)
|
2021-03-13 18:11:06 +01:00
|
|
|
else:
|
|
|
|
# If decrypting, verify MAC tag
|
|
|
|
crcdg = crc.digest()
|
|
|
|
if not compare_digest(crccs,crcdg):
|
|
|
|
# File is corrupted
|
|
|
|
statusString.set(corruptedNotice)
|
|
|
|
progress["value"] = 100
|
|
|
|
fin.close()
|
|
|
|
fout.close()
|
2021-03-15 19:41:15 +01:00
|
|
|
# If keep file not checked...
|
2021-03-13 18:11:06 +01:00
|
|
|
if keep.get()!=1:
|
|
|
|
remove(outputFile)
|
2021-03-16 17:10:54 +01:00
|
|
|
# Reset UI
|
2021-03-13 18:11:06 +01:00
|
|
|
selectFileInput["state"] = "normal"
|
|
|
|
passwordInput["state"] = "normal"
|
|
|
|
adArea["state"] = "normal"
|
|
|
|
startBtn["state"] = "normal"
|
|
|
|
keepBtn["state"] = "normal"
|
2021-03-15 19:41:15 +01:00
|
|
|
rsBtn["state"] = "normal"
|
2021-03-13 18:11:06 +01:00
|
|
|
working = False
|
|
|
|
del fin,fout,cipher,key
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
kept = "corrupted"
|
|
|
|
try:
|
2021-03-16 17:10:54 +01:00
|
|
|
# Throws ValueError if incorrect Poly1305
|
2021-03-13 18:11:06 +01:00
|
|
|
cipher.verify(digest)
|
|
|
|
except:
|
2021-03-15 19:41:15 +01:00
|
|
|
if not reedsoloErrorCount:
|
|
|
|
# File is modified
|
|
|
|
statusString.set(modifiedNotice)
|
|
|
|
progress["value"] = 100
|
|
|
|
fin.close()
|
|
|
|
fout.close()
|
|
|
|
# If keep file not checked...
|
|
|
|
if keep.get()!=1:
|
|
|
|
remove(outputFile)
|
2021-03-16 17:10:54 +01:00
|
|
|
# Reset UI
|
2021-03-15 19:41:15 +01:00
|
|
|
selectFileInput["state"] = "normal"
|
|
|
|
passwordInput["state"] = "normal"
|
|
|
|
adArea["state"] = "normal"
|
|
|
|
startBtn["state"] = "normal"
|
|
|
|
keepBtn["state"] = "normal"
|
|
|
|
rsBtn["state"] = "normal"
|
|
|
|
working = False
|
|
|
|
del fin,fout,cipher,key
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
kept = "modified"
|
|
|
|
break
|
|
|
|
|
|
|
|
# Encrypt/decrypt chunk and update CRC
|
|
|
|
if mode=="encrypt":
|
2021-03-16 17:10:54 +01:00
|
|
|
# Encrypt piece
|
2021-03-15 19:41:15 +01:00
|
|
|
data = cipher.encrypt(piece)
|
2021-03-16 17:10:54 +01:00
|
|
|
# Update checksum
|
2021-03-15 19:41:15 +01:00
|
|
|
crc.update(data)
|
|
|
|
if reedsolo:
|
2021-03-16 17:10:54 +01:00
|
|
|
# Encode using Reed-Solomon if user chooses
|
2021-03-15 19:41:15 +01:00
|
|
|
data = bytes(rsc.encode(data))
|
|
|
|
else:
|
2021-03-16 17:10:54 +01:00
|
|
|
# Basically encrypting but in reverse
|
2021-03-15 19:41:15 +01:00
|
|
|
if reedsolo:
|
|
|
|
try:
|
|
|
|
data,_,fixed = rsc.decode(piece)
|
|
|
|
except ReedSolomonError:
|
|
|
|
# File is really corrupted
|
|
|
|
if not reedsoloErrorCount:
|
|
|
|
statusString.set(corruptedNotice)
|
|
|
|
progress["value"] = 100
|
|
|
|
# If keep file not checked...
|
2021-03-13 18:11:06 +01:00
|
|
|
if keep.get()!=1:
|
2021-03-15 19:41:15 +01:00
|
|
|
fin.close()
|
|
|
|
fout.close()
|
2021-03-13 18:11:06 +01:00
|
|
|
remove(outputFile)
|
2021-03-16 17:10:54 +01:00
|
|
|
# Reset UI
|
2021-03-13 18:11:06 +01:00
|
|
|
selectFileInput["state"] = "normal"
|
|
|
|
passwordInput["state"] = "normal"
|
|
|
|
adArea["state"] = "normal"
|
|
|
|
startBtn["state"] = "normal"
|
|
|
|
keepBtn["state"] = "normal"
|
2021-03-15 19:41:15 +01:00
|
|
|
rsBtn["state"] = "normal"
|
2021-03-13 18:11:06 +01:00
|
|
|
working = False
|
|
|
|
del fin,fout,cipher,key
|
|
|
|
return
|
|
|
|
else:
|
2021-03-15 19:41:15 +01:00
|
|
|
kept = "corrupted"
|
|
|
|
data = piece[:2**20]
|
|
|
|
fixed = bytearray()
|
|
|
|
reedsoloErrorCount += 1
|
|
|
|
data = bytes(data)
|
|
|
|
reedsoloFixedCount += len(fixed)
|
|
|
|
crc.update(data)
|
|
|
|
data = cipher.decrypt(data)
|
|
|
|
else:
|
|
|
|
crc.update(piece)
|
|
|
|
data = cipher.decrypt(piece)
|
2021-03-13 18:11:06 +01:00
|
|
|
|
|
|
|
# Calculate speed, ETA, etc.
|
|
|
|
first = False
|
|
|
|
elapsed = (datetime.now()-startTime).total_seconds()
|
2021-03-16 17:10:54 +01:00
|
|
|
# Prevent divison by zero
|
|
|
|
if not elapsed:
|
2021-03-13 18:11:06 +01:00
|
|
|
elapsed = 0.1**6
|
|
|
|
percent = done*100/total
|
|
|
|
progress["value"] = percent
|
|
|
|
rPercent = round(percent)
|
|
|
|
speed = (done/elapsed)/10**6
|
2021-03-16 17:10:54 +01:00
|
|
|
# Prevent divison by zero
|
|
|
|
if not speed:
|
2021-03-13 18:11:06 +01:00
|
|
|
first = True
|
|
|
|
speed = 0.1**6
|
2021-03-15 19:41:15 +01:00
|
|
|
rSpeed = round(speed,2)
|
2021-03-13 18:11:06 +01:00
|
|
|
eta = round((total-done)/(speed*10**6))
|
2021-03-16 17:10:54 +01:00
|
|
|
# Seconds to minutes if seconds more than 59
|
2021-03-15 19:41:15 +01:00
|
|
|
if eta>=60:
|
|
|
|
eta = f"{eta//60}m {eta%60}"
|
2021-03-16 17:10:54 +01:00
|
|
|
# If it's the first round and no data/predictions yet...
|
2021-03-13 18:11:06 +01:00
|
|
|
if first:
|
|
|
|
statusString.set("...% at ... MB/s (ETA: ...s)")
|
|
|
|
else:
|
2021-03-16 17:10:54 +01:00
|
|
|
# Update status
|
2021-03-13 18:11:06 +01:00
|
|
|
info = f"{rPercent}% at {rSpeed} MB/s (ETA: {eta}s)"
|
2021-03-15 19:41:15 +01:00
|
|
|
if reedsolo and mode=="decrypt" and reedsoloFixedCount:
|
|
|
|
info += f", fixed {reedsoloFixedCount} corrupted bytes"
|
|
|
|
if reedsolo and mode=="decrypt" and reedsoloErrorCount:
|
|
|
|
info += f", {reedsoloErrorCount} MB unrecoverable"
|
2021-03-13 18:11:06 +01:00
|
|
|
statusString.set(info)
|
|
|
|
|
2021-03-16 17:10:54 +01:00
|
|
|
# Increase done and write to output
|
2021-03-13 18:11:06 +01:00
|
|
|
done += chunkSize
|
|
|
|
fout.write(data)
|
|
|
|
|
|
|
|
# Show appropriate notice if file corrupted or modified
|
|
|
|
if not kept:
|
|
|
|
if mode=="encrypt":
|
2021-03-16 20:07:25 +01:00
|
|
|
output = inputFile.split("/")[-1]+".pcv"
|
2021-03-13 18:11:06 +01:00
|
|
|
else:
|
2021-03-16 20:07:25 +01:00
|
|
|
output = inputFile.split("/")[-1].replace(".pcf","").replace(".pcv","")
|
2021-03-13 18:11:06 +01:00
|
|
|
statusString.set(f"Completed. (Output: {output})")
|
2021-03-16 17:10:54 +01:00
|
|
|
# Show Reed-Solomon stats if it fixed corrupted bytes
|
2021-03-15 19:41:15 +01:00
|
|
|
if mode=="decrypt" and reedsolo and reedsoloFixedCount:
|
|
|
|
statusString.set(f"Completed with {reedsoloFixedCount} bytes fixed."+
|
|
|
|
f" (Output: {output})")
|
2021-03-13 18:11:06 +01:00
|
|
|
else:
|
|
|
|
if kept=="modified":
|
|
|
|
statusString.set(kModifiedNotice)
|
|
|
|
else:
|
|
|
|
statusString.set(kCorruptedNotice)
|
|
|
|
|
|
|
|
# Reset variables and UI states
|
|
|
|
selectFileInput["state"] = "normal"
|
|
|
|
adArea["state"] = "normal"
|
|
|
|
adArea.delete("1.0",tkinter.END)
|
|
|
|
adArea["state"] = "disabled"
|
|
|
|
startBtn["state"] = "disabled"
|
|
|
|
passwordInput["state"] = "normal"
|
|
|
|
passwordInput.delete(0,"end")
|
|
|
|
passwordInput["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"
|
2021-03-15 19:41:15 +01:00
|
|
|
rs.set(0)
|
|
|
|
rsBtn["state"] = "disabled"
|
2021-03-13 18:11:06 +01:00
|
|
|
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 = ""
|
|
|
|
outputFile = ""
|
|
|
|
password = ""
|
|
|
|
ad = ""
|
|
|
|
kept = False
|
|
|
|
working = False
|
|
|
|
# Wipe keys for safety
|
|
|
|
del fin,fout,cipher,key
|
|
|
|
|
|
|
|
# Wraps the start() function with error handling
|
2021-02-24 17:54:34 +01:00
|
|
|
def wrapper():
|
2021-03-13 18:11:06 +01:00
|
|
|
global working
|
|
|
|
# Try start() and handle errors
|
|
|
|
try:
|
|
|
|
start()
|
2021-03-16 20:07:25 +01:00
|
|
|
except Exception:
|
2021-03-15 19:41:15 +01:00
|
|
|
progress["value"] = 100
|
2021-03-13 18:11:06 +01:00
|
|
|
selectFileInput["state"] = "normal"
|
|
|
|
passwordInput["state"] = "normal"
|
|
|
|
adArea["state"] = "normal"
|
|
|
|
startBtn["state"] = "normal"
|
|
|
|
keepBtn["state"] = "normal"
|
2021-03-15 19:41:15 +01:00
|
|
|
rsBtn["state"] = "normal"
|
2021-03-13 18:11:06 +01:00
|
|
|
statusString.set(unknownErrorNotice)
|
|
|
|
dummy.focus()
|
|
|
|
working = False
|
|
|
|
finally:
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
# Encryption/decrypt is done is a separate thread
|
|
|
|
# so the UI isn't blocked. This is a wrapper
|
|
|
|
# to spawn a thread and start it.
|
2021-02-19 01:43:18 +01:00
|
|
|
def startWorker():
|
2021-03-13 18:11:06 +01:00
|
|
|
thread = Thread(target=wrapper,daemon=True)
|
|
|
|
thread.start()
|
2021-02-19 01:43:18 +01:00
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# ad stands for "associated data"/metadata
|
2021-02-19 01:43:18 +01:00
|
|
|
adLabelString = tkinter.StringVar(tk)
|
|
|
|
adLabelString.set(adString)
|
|
|
|
adLabel = tkinter.ttk.Label(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
textvariable=adLabelString
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
adLabel.place(x=17,y=108)
|
|
|
|
adLabel.config(background="#f5f6f7")
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Frame so metadata text box can fill width
|
2021-02-19 01:43:18 +01:00
|
|
|
adFrame = tkinter.Frame(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
width=440,
|
|
|
|
height=100
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
adFrame.place(x=20,y=128)
|
|
|
|
adFrame.columnconfigure(0,weight=10)
|
|
|
|
adFrame.grid_propagate(False)
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Metadata text box
|
2021-02-19 16:30:39 +01:00
|
|
|
adArea = tkinter.Text(
|
2021-03-13 18:11:06 +01:00
|
|
|
adFrame,
|
|
|
|
exportselection=0
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
adArea.config(font=("Consolas",12))
|
|
|
|
adArea.grid(sticky="we")
|
|
|
|
adArea["state"] = "disabled"
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Check box for keeping corrupted/modified output
|
2021-02-19 01:43:18 +01:00
|
|
|
keep = tkinter.IntVar()
|
|
|
|
keepBtn = tkinter.ttk.Checkbutton(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
text=keepNotice,
|
|
|
|
variable=keep,
|
|
|
|
onvalue=1,
|
|
|
|
offvalue=0,
|
|
|
|
command=lambda:dummy.focus()
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
keepBtn.place(x=18,y=240)
|
|
|
|
keepBtn["state"] = "disabled"
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Check box for securely erasing original file
|
2021-02-19 01:43:18 +01:00
|
|
|
erase = tkinter.IntVar()
|
|
|
|
eraseBtn = tkinter.ttk.Checkbutton(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
text=eraseNotice,
|
|
|
|
variable=erase,
|
|
|
|
onvalue=1,
|
|
|
|
offvalue=0,
|
|
|
|
command=lambda:dummy.focus()
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
eraseBtn.place(x=18,y=260)
|
|
|
|
eraseBtn["state"] = "disabled"
|
|
|
|
|
2021-03-15 19:41:15 +01:00
|
|
|
# Check box for Reed Solomon
|
|
|
|
rs = tkinter.IntVar()
|
|
|
|
rsBtn = tkinter.ttk.Checkbutton(
|
|
|
|
tk,
|
|
|
|
text=rsNotice,
|
|
|
|
variable=rs,
|
|
|
|
onvalue=1,
|
|
|
|
offvalue=0,
|
|
|
|
command=lambda:dummy.focus()
|
|
|
|
)
|
|
|
|
rsBtn.place(x=18,y=280)
|
|
|
|
rsBtn["state"] = "disabled"
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Frame so start button can fill width
|
2021-02-19 01:43:18 +01:00
|
|
|
startFrame = tkinter.Frame(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
width=442,
|
|
|
|
height=25
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
2021-03-15 19:41:15 +01:00
|
|
|
startFrame.place(x=19,y=310)
|
2021-02-19 01:43:18 +01:00
|
|
|
startFrame.columnconfigure(0,weight=10)
|
|
|
|
startFrame.grid_propagate(False)
|
2021-03-13 18:11:06 +01:00
|
|
|
# Start button
|
2021-02-19 01:43:18 +01:00
|
|
|
startBtn = tkinter.ttk.Button(
|
2021-03-13 18:11:06 +01:00
|
|
|
startFrame,
|
|
|
|
text="Start",
|
|
|
|
command=startWorker
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
startBtn.grid(sticky="nesw")
|
|
|
|
startBtn["state"] = "disabled"
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Progress bar
|
2021-02-19 01:43:18 +01:00
|
|
|
progress = tkinter.ttk.Progressbar(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
orient=tkinter.HORIZONTAL,
|
|
|
|
length=440,
|
|
|
|
mode="determinate"
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
2021-03-15 19:41:15 +01:00
|
|
|
progress.place(x=20,y=348)
|
2021-02-19 01:43:18 +01:00
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Status label
|
2021-02-19 01:43:18 +01:00
|
|
|
statusString = tkinter.StringVar(tk)
|
|
|
|
statusString.set("Ready.")
|
|
|
|
status = tkinter.ttk.Label(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
textvariable=statusString
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
2021-03-15 19:41:15 +01:00
|
|
|
status.place(x=17,y=376)
|
2021-02-19 01:43:18 +01:00
|
|
|
status.config(background="#f5f6f7")
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Credits :)
|
2021-02-25 17:35:54 +01:00
|
|
|
hint = "Created by Evan Su. Click for details and source."
|
2021-02-19 16:30:39 +01:00
|
|
|
creditsString = tkinter.StringVar(tk)
|
|
|
|
creditsString.set(hint)
|
|
|
|
credits = tkinter.ttk.Label(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
textvariable=creditsString,
|
|
|
|
cursor="hand2"
|
2021-02-19 16:30:39 +01:00
|
|
|
)
|
|
|
|
credits["state"] = "disabled"
|
|
|
|
credits.config(background="#f5f6f7")
|
2021-03-15 19:41:15 +01:00
|
|
|
credits.place(x=17,y=406)
|
2021-02-19 16:30:39 +01:00
|
|
|
source = "https://github.com/HACKERALERT/Picocrypt"
|
|
|
|
credits.bind("<Button-1>",lambda e:webbrowser.open(source))
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Version
|
2021-02-24 17:54:34 +01:00
|
|
|
versionString = tkinter.StringVar(tk)
|
2021-03-15 19:41:15 +01:00
|
|
|
versionString.set("v1.8")
|
2021-02-24 17:54:34 +01:00
|
|
|
version = tkinter.ttk.Label(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk,
|
|
|
|
textvariable=versionString
|
2021-02-24 17:54:34 +01:00
|
|
|
)
|
|
|
|
version["state"] = "disabled"
|
|
|
|
version.config(background="#f5f6f7")
|
2021-03-15 19:41:15 +01:00
|
|
|
version.place(x=436,y=406)
|
2021-02-19 16:30:39 +01:00
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Dummy button to remove focus from other buttons
|
|
|
|
# and prevent ugly border highlighting
|
2021-02-19 01:43:18 +01:00
|
|
|
dummy = tkinter.ttk.Button(
|
2021-03-13 18:11:06 +01:00
|
|
|
tk
|
2021-02-19 01:43:18 +01:00
|
|
|
)
|
|
|
|
dummy.place(x=480,y=0)
|
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Close window only if not encryption or decrypting
|
2021-02-19 01:43:18 +01:00
|
|
|
def onClose():
|
2021-03-13 18:11:06 +01:00
|
|
|
if not working:
|
|
|
|
tk.destroy()
|
2021-02-19 01:43:18 +01:00
|
|
|
|
2021-03-13 18:11:06 +01:00
|
|
|
# Main tkinter loop
|
2021-02-19 01:43:18 +01:00
|
|
|
if __name__=="__main__":
|
2021-03-13 18:11:06 +01:00
|
|
|
tk.protocol("WM_DELETE_WINDOW",onClose)
|
|
|
|
tk.mainloop()
|
|
|
|
sys.exit(0)
|