v1.10 (Beta)
Better anti-corruption support, headers are encoded with Reed-Solomon by default.
This commit is contained in:
parent
06b2cde767
commit
78a68250f4
283
src/Picocrypt.py
283
src/Picocrypt.py
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue