v1.10 (Beta)

Better anti-corruption support, headers are encoded with Reed-Solomon by default.
This commit is contained in:
Evan Su 2021-03-18 15:15:32 -04:00 committed by GitHub
parent 06b2cde767
commit 78a68250f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 154 additions and 129 deletions

View File

@ -65,6 +65,7 @@ ad = ""
kept = False kept = False
working = False working = False
gMode = None gMode = None
headerRsc = None
adString = "File metadata (used to store some text along with the file):" adString = "File metadata (used to store some text along with the file):"
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."
@ -101,7 +102,7 @@ s.configure("TCheckbutton",background="#f5f6f7")
# Event when user selects an input file # Event when user selects an input file
def inputSelected(): def inputSelected():
global inputFile,working global inputFile,working,headerRsc
dummy.focus() dummy.focus()
# Try to handle when select file is cancelled # Try to handle when select file is cancelled
@ -115,27 +116,37 @@ def inputSelected():
# Exception will be caught by except below # Exception will be caught by except below
raise Exception("No file selected.") raise Exception("No file selected.")
inputFile = tmp inputFile = tmp
# Decide if encrypting or decrypting (".pcf" is the legacy Picocrypt extension,
# ".pcv" is the newer Picocrypt extension. Both are cross-compatible, but # Decide if encrypting or decrypting
# I just think ".pcv" is better because it stands for "Picocrypt Volume") if ".pcv" in inputFile.split("/")[-1]:
if ".pcf" in inputFile.split("/")[-1] or ".pcv" in inputFile.split("/")[-1]:
suffix = " (will decrypt)" suffix = " (will decrypt)"
fin = open(inputFile,"rb+") fin = open(inputFile,"r+b")
# Read file metadata
adlen = b"" # Read file metadata (a little complex)
while True: tmp = fin.read(139)
letter = fin.read(1) reedsolo = False
if letter!=b"+": if tmp[0]==43:
adlen += letter reedsolo = True
if letter==b"|": tmp = tmp[1:]
adlen = adlen[:-1] else:
break tmp = tmp[:-1]
ad = fin.read(int(adlen.decode("utf-8"))) tmp = bytes(headerRsc.decode(tmp)[0])
tmp = tmp.replace(b"+",b"")
tmp = int(tmp.decode("utf-8"))
if not reedsolo:
fin.seek(138)
ad = fin.read(tmp)
try:
ad = bytes(headerRsc.decode(ad)[0])
except ReedSolomonError:
ad = b"Error decoding file metadata."
ad = ad.decode("utf-8")
fin.close() fin.close()
# Insert the metadata into its text box # Insert the metadata into its text box
adArea["state"] = "normal" adArea["state"] = "normal"
adArea.delete("1.0",tkinter.END) adArea.delete("1.0",tkinter.END)
adArea.insert("1.0",ad.decode("utf-8")) adArea.insert("1.0",ad)
adArea["state"] = "disabled" adArea["state"] = "disabled"
adLabelString.set("File metadata (read only):") adLabelString.set("File metadata (read only):")
keepBtn["state"] = "normal" keepBtn["state"] = "normal"
@ -155,6 +166,7 @@ def inputSelected():
adLabelString.set(adString) adLabelString.set(adString)
cpasswordInput["state"] = "normal" cpasswordInput["state"] = "normal"
cpasswordInput.delete(0,"end") cpasswordInput.delete(0,"end")
# Enable password box, etc. # Enable password box, etc.
inputString.set(inputFile.split("/")[-1]+suffix) inputString.set(inputFile.split("/")[-1]+suffix)
passwordInput["state"] = "normal" passwordInput["state"] = "normal"
@ -162,14 +174,16 @@ def inputSelected():
startBtn["state"] = "normal" startBtn["state"] = "normal"
statusString.set("Ready.") statusString.set("Ready.")
progress["value"] = 0 progress["value"] = 0
# File decode error # File decode error
except UnicodeDecodeError: except UnicodeDecodeError:
passwordInput["state"] = "normal"
passwordInput.delete(0,"end")
statusString.set(corruptedNotice) statusString.set(corruptedNotice)
progress["value"] = 100
# No file selected, do nothing # No file selected, do nothing
except: except:
pass pass
# Focus the dummy button to remove ugly borders # Focus the dummy button to remove ugly borders
finally: finally:
dummy.focus() dummy.focus()
@ -248,23 +262,13 @@ 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 global inputFile,outputFile,password,ad,kept,working,gMode,headerRsc
dummy.focus() dummy.focus()
reedsolo = False reedsolo = False
chunkSize = 2**20 chunkSize = 2**20
# Disable inputs and buttons while encrypting/decrypting
selectFileInput["state"] = "disabled"
passwordInput["state"] = "disabled"
cpasswordInput["state"] = "disabled"
adArea["state"] = "disabled"
startBtn["state"] = "disabled"
eraseBtn["state"] = "disabled"
keepBtn["state"] = "disabled"
rsBtn["state"] = "disabled"
# Decide if encrypting or decrypting # Decide if encrypting or decrypting
if ".pcf" not in inputFile and ".pcv" not in inputFile: if ".pcv" not in inputFile:
mode = "encrypt" mode = "encrypt"
gMode = "encrypt" gMode = "encrypt"
outputFile = inputFile+".pcv" outputFile = inputFile+".pcv"
@ -281,6 +285,26 @@ def start():
# Decrypted output is just input file without the extension # Decrypted output is just input file without the extension
outputFile = inputFile[:-4] outputFile = inputFile[:-4]
# Check if file already exists (getsize() throws error if file not found)
try:
getsize(outputFile)
force = messagebox.askyesno("Warning",overwriteNotice)
dummy.focus()
if force!=1:
return
except:
pass
# Disable inputs and buttons while encrypting/decrypting
selectFileInput["state"] = "disabled"
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" selectFileInput["state"] = "normal"
@ -295,23 +319,13 @@ def start():
statusString.set("Passwords don't match.") statusString.set("Passwords don't match.")
return return
# Check if file already exists (getsize() throws error if file not found)
try:
getsize(outputFile)
force = messagebox.askyesno("Warning",overwriteNotice)
dummy.focus()
if force!=1:
return
except:
pass
# Set progress bar indeterminate # Set progress bar indeterminate
progress.config(mode="indeterminate") progress.config(mode="indeterminate")
progress.start(15) progress.start(15)
statusString.set(rscNotice)
# Create Reed-Solomon object # Create Reed-Solomon object
if reedsolo: if reedsolo:
statusString.set(rscNotice)
# 13 bytes per 128 bytes, ~10% larger output file # 13 bytes per 128 bytes, ~10% larger output file
rsc = RSCodec(13) rsc = RSCodec(13)
@ -332,90 +346,97 @@ def start():
fin.read(1) fin.read(1)
fout = open(outputFile,"wb+") fout = open(outputFile,"wb+")
if reedsolo and mode=="encrypt": if reedsolo and mode=="encrypt":
# Signal that Reed-Solomon was enabled with "+" # Signal that Reed-Solomon was enabled with a "+"
fout.write(b"+") fout.write(b"+")
# Generate values for encryption if encrypting # Generate values for encryption if encrypting
if mode=="encrypt": if mode=="encrypt":
salt = urandom(16) salt = urandom(16)
nonce = urandom(24) nonce = urandom(24)
fout.write(str(len(ad)).encode("utf-8")) # Length of metadata
fout.write(b"|") # Separator # Reed-Solomon-encode metadata
ad = bytes(headerRsc.encode(ad))
# Write the metadata to output
tmp = str(len(ad)).encode("utf-8")
# Right-pad with "+"
while len(tmp)!=10:
tmp += b"+"
tmp = bytes(headerRsc.encode(tmp))
fout.write(tmp) # Length of metadata
fout.write(ad) # Metadata (associated data) fout.write(ad) # Metadata (associated data)
# Write zeros as placeholder, come back to write over it later # Write zeros as placeholders, come back to write over it later.
# Note that 13 additional bytes are added if Reed-Solomon is enabled # Note that 128 extra Reed-Solomon bytes are added
fout.write(b"0"*(64+(13 if reedsolo else 0))) # SHA3-512 of encryption key fout.write(b"0"*192) # SHA3-512 of encryption key
fout.write(b"0"*(64+(13 if reedsolo else 0))) # CRC of file fout.write(b"0"*192) # CRC of file
fout.write(b"0"*(16+(13 if reedsolo else 0))) # Poly1305 tag fout.write(b"0"*144) # Poly1305 tag
# If Reed-Solomon is enabled, encode the salt and nonce, otherwise write them raw # Reed-Solomon-encode salt and nonce
fout.write(bytes(rsc.encode(salt)) if reedsolo else salt) # Argon2 salt fout.write(bytes(headerRsc.encode(salt))) # Argon2 salt
fout.write(bytes(rsc.encode(nonce)) if reedsolo else nonce) # ChaCha20 nonce fout.write(bytes(headerRsc.encode(nonce))) # ChaCha20 nonce
# If decrypting, read values from file # If decrypting, read values from file
else: else:
# Read past metadata into actual data # Move past metadata into actual data
adlen = b"" tmp = fin.read(138)
while True: if tmp[0]==43:
letter = fin.read(1) tmp = tmp[1:]+fin.read(1)
adlen += letter tmp = bytes(headerRsc.decode(tmp)[0])
if letter==b"|": tmp = tmp.replace(b"+",b"")
adlen = adlen[:-1] adlen = int(tmp.decode("utf-8"))
break fin.read(int(adlen))
fin.read(int(adlen.decode("utf-8")))
# Read the salt, nonce, etc.
# Read 13 extra bytes if Reed-Solomon is enabled
cs = fin.read(77 if reedsolo else 64)
crccs = fin.read(77 if reedsolo else 64)
digest = fin.read(29 if reedsolo else 16)
salt = fin.read(29 if reedsolo else 16)
nonce = fin.read(37 if reedsolo else 24)
# If Reed-Solomon is enabled, decode each value
if reedsolo:
try:
cs = bytes(rsc.decode(cs)[0])
except:
headerBroken = True
cs = cs[:64]
try:
crccs = bytes(rsc.decode(crccs)[0])
except:
headerBroken = True
crccs = crccs[:64]
try:
digest = bytes(rsc.decode(digest)[0])
except:
headerBroken = True
digest = digest[:16]
try:
salt = bytes(rsc.decode(salt)[0])
except:
headerBroken = True
salt = salt[:16]
try:
nonce = bytes(rsc.decode(nonce)[0])
except:
headerBroken = True
nonce = nonce[:24]
if headerBroken: # Read the salt, nonce, etc.
if keep.get()!=1: cs = fin.read(192)
statusString.set(veryCorruptedNotice) crccs = fin.read(192)
fin.close() digest = fin.read(144)
fout.close() salt = fin.read(144)
remove(outputFile) nonce = fin.read(152)
# Reset UI # Reed-Solomon-decode each value
selectFileInput["state"] = "normal" try:
passwordInput["state"] = "normal" cs = bytes(headerRsc.decode(cs)[0])
adArea["state"] = "normal" except:
startBtn["state"] = "normal" headerBroken = True
keepBtn["state"] = "normal" cs = cs[:64]
working = False try:
progress.stop() crccs = bytes(headerRsc.decode(crccs)[0])
progress.config(mode="determinate") except:
progress["value"] = 100 headerBroken = True
return crccs = crccs[:64]
else: try:
kept = "badlyCorrupted" digest = bytes(headerRsc.decode(digest)[0])
except:
headerBroken = True
digest = digest[:16]
try:
salt = bytes(headerRsc.decode(salt)[0])
except:
headerBroken = True
salt = salt[:16]
try:
nonce = bytes(headerRsc.decode(nonce)[0])
except:
headerBroken = True
nonce = nonce[:24]
if headerBroken:
if keep.get()!=1:
statusString.set(veryCorruptedNotice)
fin.close()
fout.close()
remove(outputFile)
# Reset UI
selectFileInput["state"] = "normal"
passwordInput["state"] = "normal"
adArea["state"] = "normal"
startBtn["state"] = "normal"
keepBtn["state"] = "normal"
working = False
progress.stop()
progress.config(mode="determinate")
progress["value"] = 100
return
else:
kept = "badlyCorrupted"
# Show notice about key derivation # Show notice about key derivation
statusString.set(derivingNotice) statusString.set(derivingNotice)
@ -497,19 +518,13 @@ def start():
fout.flush() fout.flush()
fout.close() fout.close()
fout = open(outputFile,"r+b") fout = open(outputFile,"r+b")
# Compute the offset and seek to it # Compute the offset and seek to it (unshift "+")
rsOffset = 1 if reedsolo else 0 rsOffset = 1 if reedsolo else 0
fout.seek(len(str(len(ad)))+1+len(ad)+rsOffset) fout.seek(138+len(ad)+rsOffset)
# Write hash of key, CRC, and Poly1305 MAC tag # Write hash of key, CRC, and Poly1305 MAC tag
# Reed-Solomon-encode if selected by user fout.write(bytes(headerRsc.encode(check)))
if reedsolo: fout.write(bytes(headerRsc.encode(crc.digest())))
fout.write(bytes(rsc.encode(check))) fout.write(bytes(headerRsc.encode(digest)))
fout.write(bytes(rsc.encode(crc.digest())))
fout.write(bytes(rsc.encode(digest)))
else:
fout.write(check)
fout.write(crc.digest())
fout.write(digest)
else: else:
# If decrypting, verify CRC # If decrypting, verify CRC
crcdg = crc.digest() crcdg = crc.digest()
@ -638,7 +653,7 @@ def start():
first = True first = True
speed = 0.1**6 speed = 0.1**6
rSpeed = str(round(speed,2)) rSpeed = str(round(speed,2))
# Right-pad zeros to prevent layout shifts # Right-pad with zeros to large prevent layout shifts
while len(rSpeed.split(".")[1])!=2: while len(rSpeed.split(".")[1])!=2:
rSpeed += "0" rSpeed += "0"
eta = round((total-done)/(speed*10**6)) eta = round((total-done)/(speed*10**6))
@ -670,7 +685,7 @@ def start():
if mode=="encrypt": if mode=="encrypt":
output = inputFile.split("/")[-1]+".pcv" output = inputFile.split("/")[-1]+".pcv"
else: else:
output = inputFile.split("/")[-1].replace(".pcf","").replace(".pcv","") output = inputFile.split("/")[-1].replace(".pcv","")
statusString.set(f"Completed. (Output: {output})") 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:
@ -900,13 +915,23 @@ dummy = tkinter.ttk.Button(
) )
dummy.place(x=480,y=0) dummy.place(x=480,y=0)
# Function to create Reed-Solomon header codec
def createRsc():
global headerRsc
headerRsc = RSCodec(128)
sys.exit(0)
# Close window only if not encrypting or decrypting # Close window only if not encrypting or decrypting
def onClose(): def onClose():
if not working: if not working:
tk.destroy() tk.destroy()
# Main tkinter loop # Main application loop
if __name__=="__main__": if __name__=="__main__":
# Create Reed-Solomon header codec
tmp = Thread(target=createRsc,daemon=True)
tmp.start()
# Start tkinter
tk.protocol("WM_DELETE_WINDOW",onClose) tk.protocol("WM_DELETE_WINDOW",onClose)
tk.mainloop() tk.mainloop()
sys.exit(0) sys.exit(0)