def test_hash_nul_bytes(self): """ Hashing secrets with NUL bytes works as expected. """ params = ( TEST_SALT, TEST_TIME, TEST_MEMORY, TEST_PARALLELISM, TEST_HASH_LEN, Type.I, ) rv = hash_secret_raw(b"abc\x00", *params) assert rv != hash_secret_raw(b"abc", *params)
def __init__(self, password, salt=None, hash_len=32, salt_len=16, time_cost=500, memory_cost=1000, parallelism=4, argon2_type=low_level.Type.I, version=19): """ args: password: password as bytes salt: salt in bytes or None to create random one, must have length >= 8 hash_len: generated hash length in bytes salt_len: salt length in bytes, ignored if salt is not None, must be >= 8 Other arguments are argon2 settings. Only change those if you know what you're doing. Optimized for slow hashing suitable for file encryption. """ # Default and recommended settings from argon2.PasswordHasher are for # interactive logins. For encryption we want something much slower. self.settings = { 'time_cost': time_cost, 'memory_cost': memory_cost, 'parallelism': parallelism, 'hash_len': hash_len, 'type': argon2_type, 'version': version } self.salt = salt if salt is not None else get_random_bytes(salt_len) self.hash = low_level.hash_secret_raw(password, self.salt, **self.settings)
def peek(hidden, password, expires=None): password = ensure_bytes(password) server, security, salt, token = ensure_unicode(hidden).split('$') server = int(server) security = int(security) salt = base64_to_bytes(ensure_bytes(salt)) token = base64_to_bytes(ensure_bytes(token)) hashed = hash_secret_raw(password, salt, hash_len=HASH_LENGTH, parallelism=THREADS, type=Type.I if server else Type.D, **SECURITY_LEVELS[security]) try: secret = Fernet(urlsafe_b64encode(hashed)).decrypt(token, expires) except InvalidToken: raise ValueError( 'Unable to decrypt secret. The means either the password is wrong,' ' the password was attempted on a different hidden secret, or the ' 'secret was encrypted more than {} seconds ago.'.format(expires)) return secret
def verify_secret(secret: str, known_hash: bytes, known_salt: bytes) -> bool: """ Checks whether the given secret (a utf8 string) hashes (using the given salt) to the given known hash. Returns whether the hashes match. """ unknown_bytes = secret.encode(SECRET_ENCODING) unknown_hash = hash_secret_raw(unknown_bytes, known_salt, **CRYPTO_PARAMS) return unknown_hash == known_hash
def hash_secret(secret: str) -> Tuple[bytes, bytes]: """ Hashes the given secret (a utf8 string), returning a tuple of the derived hash and the newly-generated salt used. """ secret_bytes = secret.encode(SECRET_ENCODING) secret_salt = os.urandom(CRYPTO_SALT_LEN) secret_hash = hash_secret_raw(secret_bytes, secret_salt, **CRYPTO_PARAMS) return secret_hash, secret_salt
def test_core(): """ If called with equal parameters, core() will return the same as hash_secret(). """ pwd = b"secret" salt = b"12345678" hash_len = 8 # Keep FFI objects alive throughout the function. cout = ffi.new("uint8_t[]", hash_len) cpwd = ffi.new("uint8_t[]", pwd) csalt = ffi.new("uint8_t[]", salt) ctx = ffi.new( "argon2_context *", dict( out=cout, outlen=hash_len, version=ARGON2_VERSION, pwd=cpwd, pwdlen=len(pwd), salt=csalt, saltlen=len(salt), secret=ffi.NULL, secretlen=0, ad=ffi.NULL, adlen=0, t_cost=1, m_cost=8, lanes=1, threads=1, allocate_cbk=ffi.NULL, free_cbk=ffi.NULL, flags=lib.ARGON2_DEFAULT_FLAGS, ), ) rv = core(ctx, Type.D.value) assert 0 == rv assert ( hash_secret_raw( pwd, salt=salt, time_cost=1, memory_cost=8, parallelism=1, hash_len=hash_len, type=Type.D, ) == bytes(ffi.buffer(ctx.out, ctx.outlen)) )
def update_key(self,password): if not self.username: raise ValueError("Username must be set before key derivation") if not os.path.isfile(self.user_salt_file): raise ValueError("User directory must be populated before key derivation") with open(self.user_salt_file,"rb") as fil: key_salt=fil.read() key_val=hash_secret_raw(password.encode("utf-8"),key_salt, 10,409600,4, 16, Type.ID) self.key=key_val
def test_core(): """ If called with equal parameters, core() will return the same as hash_secret(). """ pwd = b"secret" salt = b"12345678" hash_len = 8 # Keep FFI objects alive throughout the function. cout = ffi.new("uint8_t[]", hash_len) cpwd = ffi.new("uint8_t[]", pwd) csalt = ffi.new("uint8_t[]", salt) ctx = ffi.new( "argon2_context *", dict( out=cout, outlen=hash_len, version=ARGON2_VERSION, pwd=cpwd, pwdlen=len(pwd), salt=csalt, saltlen=len(salt), secret=ffi.NULL, secretlen=0, ad=ffi.NULL, adlen=0, t_cost=1, m_cost=8, lanes=1, threads=1, allocate_cbk=ffi.NULL, free_cbk=ffi.NULL, flags=lib.ARGON2_DEFAULT_FLAGS, ) ) rv = core(ctx, Type.D.value) assert 0 == rv assert hash_secret_raw( pwd, salt=salt, time_cost=1, memory_cost=8, parallelism=1, hash_len=hash_len, type=Type.D, ) == bytes(ffi.buffer(ctx.out, ctx.outlen))
def hide(secret, password, security=2, salt=None, server=True): password = ensure_bytes(password) salt = salt or urandom(SALT_LENGTH) hashed = hash_secret_raw( password, salt, hash_len=HASH_LENGTH, parallelism=THREADS, type=Type.I if server else Type.D, **SECURITY_LEVELS[security] ) token = Fernet(urlsafe_b64encode(hashed)).encrypt(secret) return u'{}${}${}${}'.format( int(server), security, bytes_to_base64(salt), bytes_to_base64(token) )
def derive_key(pwd, time_cost, memory_cost): "Derive an encryption key using Argon2id" # we don't use a salt here - why? # we're using this to derive an encryption key, not to hash passwords # rainbow tables are not a concern pwd = pwd.encode("utf8") return hash_secret_raw(pwd, b"\x00" * 16, time_cost, memory_cost, 8, 32, Type.ID, version=19)
def derive_key(self, password): salt = binascii.unhexlify(self.config["salt"]) pw_bytes = bytes(password, "utf-8") dk = hash_secret_raw( pw_bytes, salt, time_cost=ARGON2_TIME_COST, memory_cost=ARGON2_MEMORY_COST, parallelism=ARGON2_PARALLELISM, hash_len=ARGON2_HASH_LEN, type=ARGON2_TYPE, ) return dk
def test_hash_secret_raw(self, type, hash): """ Creates the same raw hash as the Argon2 CLI client. """ rv = hash_secret_raw( TEST_PASSWORD, TEST_SALT, TEST_TIME, TEST_MEMORY, TEST_PARALLELISM, TEST_HASH_LEN, type, ) assert hash == rv assert isinstance(rv, bytes)
def _create_cipher(self, password, salt, nonce=None): """ Create the cipher object to encrypt or decrypt a payload. """ from argon2.low_level import hash_secret_raw, Type from Crypto.Cipher import AES aesmode = self._get_mode(self.aesmode) if aesmode is None: # pragma: no cover raise ValueError('invalid AES mode: %s' % self.aesmode) key = hash_secret_raw(secret=password.encode(self.password_encoding), salt=salt, time_cost=self.time_cost, memory_cost=self.memory_cost, parallelism=self.parallelism, hash_len=16, type=Type.ID) return AES.new(key, aesmode, nonce)
def POW_HASH_FUNCTION(data): return hash_secret_raw(data, ARGON2_SALT, ARGON2_ITERATIONS, ARGON2_MEMORY, ARGON2_PARALLELISM, ARGON2_SIZE, Type.I)
def start(): global inputFile, outputFile, password, ad, kept global working, gMode, headerRsc, allFiles, files global dragFolderPath dummy.focus() reedsolo = False chunkSize = 2**20 # Decide if encrypting or decrypting if not inputFile.endswith(".pcv"): mode = "encrypt" gMode = "encrypt" outputFile = inputFile + ".pcv" reedsolo = rs.get() == 1 else: mode = "decrypt" gMode = "decrypt" # Check if Reed-Solomon was enabled by checking for "+" test = open(inputFile, "rb") decider = test.read(1).decode("utf-8") test.close() if decider == "+": reedsolo = True # Decrypted output is just input file without the extension outputFile = inputFile[:-4] # Check if file already exists (getsize() throws error if file not found) try: getsize(outputFile) force = messagebox.askyesno("Confirmation", overwriteNotice) dummy.focus() if force != 1: return except: pass # Disable inputs and buttons while encrypting/decrypting disableAllInputs() # Make sure passwords match if passwordInput.get() != cpasswordInput.get() and mode == "encrypt": resetEncryptionUI() statusString.set("Passwords don't match.") return # Set progress bar indeterminate progress.config(mode="indeterminate") progress.start(15) statusString.set(rscNotice) # Create Reed-Solomon object if reedsolo: # 13 bytes per 128 bytes, ~10% larger output file 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 working = True headerBroken = False reedsoloFixedCount = 0 reedsoloErrorCount = 0 dummy.focus() password = passwordInput.get().encode("utf-8") ad = adArea.get("1.0", tkinter.END).encode("utf-8") wipe = erase.get() == 1 # Open files try: fin = open(inputFile, "rb") except: resetEncryptionUI() statusString.set("Folder is empty.") return if reedsolo and mode == "decrypt": # Move pointer one forward fin.read(1) fout = open(outputFile, "wb+") if reedsolo and mode == "encrypt": # Signal that Reed-Solomon was enabled with a "+" fout.write(b"+") # Generate values for encryption if encrypting if mode == "encrypt": salt = urandom(16) nonce = urandom(24) # 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) # Write zeros as placeholders, come back to write over it later. # Note that 128 extra Reed-Solomon bytes are added fout.write(b"0" * 192) # SHA3-512 of encryption key fout.write(b"0" * 192) # CRC of file fout.write(b"0" * 144) # Poly1305 tag # Reed-Solomon-encode salt and nonce fout.write(bytes(headerRsc.encode(salt))) # Argon2 salt fout.write(bytes(headerRsc.encode(nonce))) # ChaCha20 nonce # If decrypting, read values from file else: # Move past metadata into actual data tmp = fin.read(138) if tmp[0] == 43: tmp = tmp[1:] + fin.read(1) tmp = bytes(headerRsc.decode(tmp)[0]) tmp = tmp.replace(b"+", b"") adlen = int(tmp.decode("utf-8")) fin.read(int(adlen)) # Read the salt, nonce, etc. cs = fin.read(192) crccs = fin.read(192) digest = fin.read(144) salt = fin.read(144) nonce = fin.read(152) # Reed-Solomon-decode each value try: cs = bytes(headerRsc.decode(cs)[0]) except: headerBroken = True cs = cs[:64] try: crccs = bytes(headerRsc.decode(crccs)[0]) except: headerBroken = True crccs = crccs[:64] try: 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 resetDecryptionUI() return else: kept = "badlyCorrupted" # Show notice about key derivation statusString.set(derivingNotice) # Derive argon2id key key = hash_secret_raw( password, salt, time_cost=8, # 8 iterations memory_cost=2**20, # 2^20 Kibibytes (1GiB) 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.new() check.update(key) check = check.digest() # If decrypting, check if key is correct if mode == "decrypt": # If key is incorrect... if not compare_digest(check, cs): if not headerBroken: statusString.set(passwordNotice) fin.close() fout.close() remove(outputFile) # Reset UI resetDecryptionUI() return # Create XChaCha20-Poly1305 object cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) # Cyclic redundancy check for file corruption crc = sha3_512.new() # Amount of data encrypted/decrypted, total file size, starting time done = 0 total = getsize(inputFile) # If secure wipe enabled, create a wiper object # Keep track of time because it flies... startTime = datetime.now() previousTime = datetime.now() # Continously read file in chunks of 1MB while True: if mode == "decrypt" and reedsolo: # Read a chunk plus Reed-Solomon recovery bytes piece = fin.read(1104905) else: piece = fin.read(chunkSize) # If EOF if not piece: if mode == "encrypt": # Get the cipher MAC tag (Poly1305) digest = cipher.digest() fout.flush() fout.close() fout = open(outputFile, "r+b") # Compute the offset and seek to it (unshift "+") rsOffset = 1 if reedsolo else 0 fout.seek(138 + len(ad) + rsOffset) # Write hash of key, CRC, and Poly1305 MAC tag fout.write(bytes(headerRsc.encode(check))) fout.write(bytes(headerRsc.encode(crc.digest()))) fout.write(bytes(headerRsc.encode(digest))) else: # If decrypting, verify CRC crcdg = crc.digest() if not compare_digest(crccs, crcdg): # File is corrupted statusString.set(corruptedNotice) progress["value"] = 100 fin.close() fout.close() # If keep file not checked... if keep.get() != 1: remove(outputFile) # Reset UI resetDecryptionUI() del fin, fout, cipher, key return else: if not kept: kept = "corrupted" # Next, verify MAC tag (Poly1305) try: # Throws ValueError if incorrect Poly1305 cipher.verify(digest) except: if not reedsoloErrorCount and not headerBroken: # File is modified statusString.set(modifiedNotice) progress["value"] = 100 fin.close() fout.close() # If keep file not checked... if keep.get() != 1: remove(outputFile) # Reset UI resetDecryptionUI() del fin, fout, cipher, key return else: if not kept: kept = "modified" break # Encrypt/decrypt chunk and update CRC if mode == "encrypt": # Encrypt piece data = cipher.encrypt(piece) # Update checksum crc.update(data) if reedsolo: # Encode using Reed-Solomon if user chooses data = bytes(rsc.encode(data)) else: # Basically encrypting but in reverse if reedsolo: try: data, _, fixed = rsc.decode(piece) except ReedSolomonError: # File is really corrupted if not reedsoloErrorCount: if keep.get() != 1: statusString.set(veryCorruptedNotice) progress["value"] = 100 # If keep file not checked... if keep.get() != 1: fin.close() fout.close() remove(outputFile) # Reset UI resetDecryptionUI() del fin, fout, cipher, key return else: kept = "badlyCorrupted" # Attempt to recover badly corrupted data data = b"" piece = piece[:-13] counter = 0 while True: # Basically just strip the Reed-Solomon bytes # and return the original non-encoded data if counter < 1104905: data += piece[counter:counter + 242] counter += 255 # 255 bytes, 242 original else: break 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) # Calculate speed, ETA, etc. elapsed = (datetime.now() - previousTime).total_seconds() or 0.0001 sinceStart = (datetime.now() - startTime).total_seconds() or 0.0001 previousTime = datetime.now() percent = done * 100 / total progress["value"] = percent speed = (done / sinceStart) / 10**6 or 0.0001 eta = round((total - done) / (speed * 10**6)) # Seconds to minutes if seconds more than 59 if eta >= 60: # Set blank ETA if just starting if sinceStart < 0.5: eta = "..." else: eta = f"{eta//60}m {eta%60}" if isinstance(eta, int) or isinstance(eta, float): if eta < 0: eta = 0 # Update status info = f"{percent:.0f}% at {speed:.2f} MB/s (ETA: {eta}s)" if reedsolo and mode == "decrypt" and reedsoloFixedCount: tmp = "s" if reedsoloFixedCount != 1 else "" info += f", fixed {reedsoloFixedCount} corrupted byte{tmp}" if reedsolo and mode == "decrypt" and reedsoloErrorCount: info += f", {reedsoloErrorCount} MB unrecoverable" statusString.set(info) # Increase done and write to output done += 1104905 if (reedsolo and mode == "decrypt") else chunkSize 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 if not kept: statusString.set(f"Completed. (Click here to show output)") # Show Reed-Solomon stats if it fixed corrupted bytes if mode == "decrypt" and reedsolo and reedsoloFixedCount: statusString.set(f"Completed with {reedsoloFixedCount}" + f" bytes fixed. (Output: {output})") else: if kept == "modified": statusString.set(kModifiedNotice) elif kept == "corrupted": statusString.set(kCorruptedNotice) else: 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 resetUI() status["state"] = "normal" inputFile = "" outputFile = "" password = "" ad = "" kept = False working = False allFiles = False dragFolderPath = False # Wipe keys for safety del fin, fout, cipher, key
def start(): global inputFile,outputFile,password,ad,kept,working,gMode,headerRsc dummy.focus() reedsolo = False chunkSize = 2**20 # Decide if encrypting or decrypting if ".pcv" not in inputFile: mode = "encrypt" gMode = "encrypt" outputFile = inputFile+".pcv" reedsolo = rs.get()==1 else: mode = "decrypt" gMode = "decrypt" # Check if Reed-Solomon was enabled by checking for "+" test = open(inputFile,"rb") decider = test.read(1).decode("utf-8") test.close() if decider=="+": reedsolo = True # Decrypted output is just input file without the extension 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 if passwordInput.get()!=cpasswordInput.get() and mode=="encrypt": selectFileInput["state"] = "normal" 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.") return # Set progress bar indeterminate progress.config(mode="indeterminate") progress.start(15) statusString.set(rscNotice) # Create Reed-Solomon object if reedsolo: # 13 bytes per 128 bytes, ~10% larger output file rsc = RSCodec(13) # Set and get some variables working = True headerBroken = False reedsoloFixedCount = 0 reedsoloErrorCount = 0 dummy.focus() password = passwordInput.get().encode("utf-8") ad = adArea.get("1.0",tkinter.END).encode("utf-8") wipe = erase.get()==1 # Open files fin = open(inputFile,"rb") if reedsolo and mode=="decrypt": # Move pointer one forward fin.read(1) fout = open(outputFile,"wb+") if reedsolo and mode=="encrypt": # Signal that Reed-Solomon was enabled with a "+" fout.write(b"+") # Generate values for encryption if encrypting if mode=="encrypt": salt = urandom(16) nonce = urandom(24) # 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) # Write zeros as placeholders, come back to write over it later. # Note that 128 extra Reed-Solomon bytes are added fout.write(b"0"*192) # SHA3-512 of encryption key fout.write(b"0"*192) # CRC of file fout.write(b"0"*144) # Poly1305 tag # Reed-Solomon-encode salt and nonce fout.write(bytes(headerRsc.encode(salt))) # Argon2 salt fout.write(bytes(headerRsc.encode(nonce))) # ChaCha20 nonce # If decrypting, read values from file else: # Move past metadata into actual data tmp = fin.read(138) if tmp[0]==43: tmp = tmp[1:]+fin.read(1) tmp = bytes(headerRsc.decode(tmp)[0]) tmp = tmp.replace(b"+",b"") adlen = int(tmp.decode("utf-8")) fin.read(int(adlen)) # Read the salt, nonce, etc. cs = fin.read(192) crccs = fin.read(192) digest = fin.read(144) salt = fin.read(144) nonce = fin.read(152) # Reed-Solomon-decode each value try: cs = bytes(headerRsc.decode(cs)[0]) except: headerBroken = True cs = cs[:64] try: crccs = bytes(headerRsc.decode(crccs)[0]) except: headerBroken = True crccs = crccs[:64] try: 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 statusString.set(derivingNotice) # Derive argon2id key key = hash_secret_raw( password, salt, time_cost=8, # 8 iterations memory_cost=2**20, # 2^20 Kibibytes (1GiB) 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.new() check.update(key) check = check.digest() # If decrypting, check if key is correct if mode=="decrypt": # If key is incorrect... if not compare_digest(check,cs): if not headerBroken: statusString.set(passwordNotice) 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["value"] = 100 del key return # Create XChaCha20-Poly1305 object cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce) # Cyclic redundancy check for file corruption crc = sha3_512.new() # Amount of data encrypted/decrypted, total file size, starting time done = 0 total = getsize(inputFile) # 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... startTime = datetime.now() previousTime = datetime.now() # Continously read file in chunks of 1MB while True: if mode=="decrypt" and reedsolo: # Read a chunk plus Reed-Solomon recovery bytes piece = fin.read(1104905) else: 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 not piece: if mode=="encrypt": # Get the cipher MAC tag (Poly1305) digest = cipher.digest() fout.flush() fout.close() fout = open(outputFile,"r+b") # Compute the offset and seek to it (unshift "+") rsOffset = 1 if reedsolo else 0 fout.seek(138+len(ad)+rsOffset) # Write hash of key, CRC, and Poly1305 MAC tag fout.write(bytes(headerRsc.encode(check))) fout.write(bytes(headerRsc.encode(crc.digest()))) fout.write(bytes(headerRsc.encode(digest))) else: # If decrypting, verify CRC crcdg = crc.digest() if not compare_digest(crccs,crcdg): # File is corrupted statusString.set(corruptedNotice) progress["value"] = 100 fin.close() fout.close() # If keep file not checked... if keep.get()!=1: remove(outputFile) # Reset UI selectFileInput["state"] = "normal" passwordInput["state"] = "normal" adArea["state"] = "normal" startBtn["state"] = "normal" keepBtn["state"] = "normal" working = False del fin,fout,cipher,key return else: if not kept: kept = "corrupted" # Next, verify MAC tag (Poly1305) try: # Throws ValueError if incorrect Poly1305 cipher.verify(digest) except: if not reedsoloErrorCount and not headerBroken: # File is modified statusString.set(modifiedNotice) progress["value"] = 100 fin.close() fout.close() # If keep file not checked... if keep.get()!=1: remove(outputFile) # Reset UI selectFileInput["state"] = "normal" passwordInput["state"] = "normal" adArea["state"] = "normal" startBtn["state"] = "normal" keepBtn["state"] = "normal" working = False del fin,fout,cipher,key return else: if not kept: kept = "modified" break # Encrypt/decrypt chunk and update CRC if mode=="encrypt": # Encrypt piece data = cipher.encrypt(piece) # Update checksum crc.update(data) if reedsolo: # Encode using Reed-Solomon if user chooses data = bytes(rsc.encode(data)) else: # Basically encrypting but in reverse if reedsolo: try: data,_,fixed = rsc.decode(piece) except ReedSolomonError: # File is really corrupted if not reedsoloErrorCount: if keep.get()!=1: statusString.set(veryCorruptedNotice) progress["value"] = 100 # If keep file not checked... if keep.get()!=1: 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["value"] = 100 del fin,fout,cipher,key return else: kept = "badlyCorrupted" # Attempt to recover badly corrupted data data = b"" piece = piece[:-13] counter = 0 while True: # Basically just strip the Reed-Solomon bytes # and return the original non-encoded data if counter<1104905: data += piece[counter:counter+242] counter += 255 # 255 bytes, 242 original else: break 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) # Calculate speed, ETA, etc. first = False elapsed = (datetime.now()-previousTime).total_seconds() or 0.0001 sinceStart = (datetime.now()-startTime).total_seconds() or 0.0001 previousTime = datetime.now() # Prevent divison by zero if not elapsed: elapsed = 0.1**6 percent = done*100/total progress["value"] = percent rPercent = round(percent) speed = (done/sinceStart)/10**6 # 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)) # Seconds to minutes if seconds more than 59 if eta>=60: eta = f"{eta//60}m {eta%60}" if isinstance(eta,int) or isinstance(eta,float): if 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 info = f"{rPercent}% at {rSpeed} MB/s (ETA: {eta}s)" if reedsolo and mode=="decrypt" and reedsoloFixedCount: eng = "s" if reedsoloFixedCount!=1 else "" info += f", fixed {reedsoloFixedCount} corrupted byte{eng}" if reedsolo and mode=="decrypt" and reedsoloErrorCount: info += f", {reedsoloErrorCount} MB unrecoverable" statusString.set(info) # Increase done and write to output done += 1104905 if (reedsolo and mode=="decrypt") else chunkSize fout.write(data) # Show appropriate notice if file corrupted or modified if not kept: if mode=="encrypt": 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 if mode=="decrypt" and reedsolo and reedsoloFixedCount: statusString.set(f"Completed with {reedsoloFixedCount} bytes fixed."+ f" (Output: {output})") else: if kept=="modified": statusString.set(kModifiedNotice) elif kept=="corrupted": statusString.set(kCorruptedNotice) else: statusString.set(kVeryCorruptedNotice) # 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" 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 = "" outputFile = "" password = "" ad = "" kept = False working = False # Wipe keys for safety del fin,fout,cipher,key
def run(data: bytes, nonce: int) -> bytes: result: bytes = argon2.hash_secret_raw(data, nonce.to_bytes(8, "little"), 1, 8, 1, 32, argon2.Type.D) return result
def work(): global inputFile, outputFile, working, mode, rs13, rs128, reedsolo global done, stopUpdating, startTime, previousTime, onlyFiles global onlyFolders, allFiles, reedsoloFixed, reedsoloErrors disableAllInputs() dummy.focus() # Set and get some variables kept = False shouldKeep = keep.get() == 1 shouldErase = erase.get() == 1 reedsolo = rs.get() == 1 working = True stopUpdating = False headerBroken = False reedsoloFixed = 0 reedsoloErrors = 0 password = passwordInput.get().encode("utf-8") metadata = metadataInput.get("1.0", tkinter.END).encode("utf-8") cancelBtn["state"] = "normal" cancelBtn.config(cursor="hand2") # Decide if encrypting or decrypting if mode == "encrypt": outputFile = outputInput.get() + ".pcv" else: outputFile = outputInput.get() # Set progress bar indeterminate progress.config(mode="indeterminate") progress.start(15) # Compress files together if necessary if onlyFiles or allFiles: statusString.set(strings[1]) tmp = outputFile[:-4] if onlyFiles: zfPath = Path(onlyFiles[0]).parent.absolute() else: zfPath = Path(dirname(allFiles[0])).parent.absolute() zfOffset = len(str(zfPath)) zfName = pathJoin(zfPath, tmp) zf = ZipFile(zfName, "w") for i in allFiles: zf.write(i, i[zfOffset:]) for i in onlyFiles: zf.write(i, pathSplit(i)[1]) zf.close() inputFile = zfName outputFile = zfName + ".pcv" outputPath = dirname(outputFile) # Open files try: fin = open(inputFile, "rb") except: setEncryptionUI() statusString.set(strings[16]) return # If encrypting, generate values for encryption if mode == "encrypt": salt = urandom(16) # Argon2 salt nonce = urandom(24) # XChaCha20 nonce fout = open(outputFile, "wb+") # Indicate Reed-Solomon with "+" if reedsolo: fout.write(rs128.encode(b"+")) else: fout.write(rs128.encode(b"-")) # Encode metadata and length of metadata metadata = rs128.encode(metadata) tmp = len(metadata) tmp = f"{tmp:+<10}" tmp = rs128.encode(tmp.encode("utf-8")) # Write to file fout.write(tmp) fout.write(metadata) fout.write(rs128.encode(salt)) # Argon2 salt fout.write(rs128.encode(nonce)) # XChaCha20 nonce fout.write(b"0" * 192) # Hash of key fout.write(b"0" * 144) # Poly1305 MAC fout.write(b"0" * 160) # BLAKE3 CRC # If decrypting, read values from file else: tmp = fin.read(129) try: if bytes(rs128.decode(tmp)[0]) == b"+": reedsolo = True else: reedsolo = False except: setDecryptionUI() statusString.set(strings[21]) return metadataLength = fin.read(138) metadataLength = bytes(rs128.decode(metadataLength)[0]) metadataLength = metadataLength.replace(b"+", b"") fin.read(int(metadataLength.decode("utf-8"))) # Read values salt = fin.read(144) nonce = fin.read(152) keycs = fin.read(192) maccs = fin.read(144) crccs = fin.read(160) # Try to decode each value, increase Reed-Solomon errors fixed if needed try: salt, _, fixed = rs128.decode(salt) salt = bytes(salt) reedsoloFixed += len(fixed) except: headerBroken = True salt = salt[:16] try: nonce, _, fixed = rs128.decode(nonce) nonce = bytes(nonce) reedsoloFixed += len(fixed) except: headerBroken = True nonce = nonce[:24] try: keycs, _, fixed = rs128.decode(keycs) keycs = bytes(keycs) reedsoloFixed += len(fixed) except: headerBroken = True keycs = keycs[:64] try: maccs, _, fixed = rs128.decode(maccs) maccs = bytes(maccs) reedsoloFixed += len(fixed) except: headerBroken = True maccs = maccs[:16] try: crccs, _, fixed = rs128.decode(crccs) crccs = bytes(crccs) reedsoloFixed += len(fixed) except: headerBroken = True crccs = crccs[:32] # If the header is broken... if headerBroken: # Stop if user chose not to keep broken output if not shouldKeep: statusString.set(strings[4]) fin.close() try: remove(outputFile) except: pass setDecryptionUI() return else: kept = "badlyCorrupted" statusString.set(strings[9]) # Generate Argon2d key from master password key = hash_secret_raw(password, salt, time_cost=8, memory_cost=2**20, parallelism=8, hash_len=32, type=argonType.D) # Stop the indeterminate progress bar and set determinate progress.stop() progress.config(mode="determinate") progress["value"] = 0 # Hash of the derived Argon2 key check = SHA3_512.new(data=key).digest() # Check if password is correct if mode == "decrypt": if not compare_digest(check, keycs): # If header isn't broken... if not headerBroken: # Tell user password is incorrect statusString.set(strings[2]) fin.close() setDecryptionUI() return fout = open(outputFile, "wb+") crc = blake3() # Blake3 CRC cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) # XChaCha20 # Variables for calculating speeds, etc. done = 0 total = getsize(inputFile) startTime = datetime.now() previousTime = datetime.now() # Update progress bar, etc. in another thread Thread(target=updateStats, daemon=True, args=(total, )).start() # Start the encryption/decryption process while True: # Check if cancel button pressed if not working: fin.close() fout.close() remove(outputFile) if mode == "encrypt": setEncryptionUI() else: setDecryptionUI() statusString.set("Operation canceled by user.") dummy.focus() return # Read from file, read extra if Reed-Solomon was enabled if mode == "decrypt" and reedsolo: piece = fin.read(1104905) else: piece = fin.read(2**20) # End of file if not piece: break # Encrypt, etc. if mode == "encrypt": data = cipher.encrypt(piece) crc.update(data) if reedsolo: data = bytes(rs13.encode(data)) # Decrypt, etc. else: if reedsolo: try: data, _, fixed = rs13.decode(piece) except ReedSolomonError: # File is really corrupted if not reedsoloErrors and not shouldKeep: stopUpdating = True statusString.set(strings[4]) fin.close() fout.close() remove(outputFile) setDecryptionUI() return # Attempt to recover badly corrupted data kept = "badlyCorrupted" data = b"" piece = piece[:-13] counter = 0 while True: # Basically just strip off the Reed-Solomon bytes if counter < 1104905: data += piece[counter:counter + 242] counter += 255 # 242 bytes + 13 Reed-Solomon else: break fixed = bytearray() reedsoloErrors += 1 reedsoloFixed += len(fixed) crc.update(data) data = cipher.decrypt(data) else: crc.update(piece) data = cipher.decrypt(piece) # Write the data, increase the amount done fout.write(data) done += 1104905 if (mode == "decrypt" and reedsolo) else 2**20 # Stop UI updater from overwriting potential messages stopUpdating = True # Encryption is done, write appropriate values to file if mode == "encrypt": fout.flush() fout.close() fout = open(outputFile, "r+b") fout.seek(129 + 138 + len(metadata) + 144 + 152) fout.write(rs128.encode(check)) fout.write(rs128.encode(cipher.digest())) fout.write(rs128.encode(crc.digest())) # Decryption is done, check for integrity and authenticity else: # File is corrupted if not compare_digest(crccs, crc.digest()): statusString.set(strings[3]) fin.close() fout.close() if keep.get() != 1: remove(outputFile) setDecryptionUI() return else: if not kept: kept = "corrupted" try: cipher.verify(maccs) except: if not reedsoloErrors and not headerBroken: # File is modified statusString.set(strings[5]) fin.close() fout.close() # If keep file not checked... if keep.get() != 1: remove(outputFile) # Reset UI setDecryptionUI() return else: if not kept: kept = "modified" # Flush outputs, close files if not kept: fout.flush() fsync(fout.fileno()) fout.close() fin.close() # Securely wipe files as necessary if shouldErase: if onlyFolders: for i in onlyFolders: secureWipe(i) if onlyFiles: for i in range(len(onlyFiles)): statusString.set(strings[12] + f" ({i}/{len(onlyFiles)}") progress["value"] = i / len(onlyFiles) secureWipe(onlyFiles[i]) secureWipe(inputFile) # Secure wipe not enabled else: # Remove temporary zip file if created if allFiles or onlyFiles: remove(inputFile) # Show appropriate notice if file corrupted or modified arrow = "" if platform.system() == "Darwin" else "🡪" if not kept: statusString.set(f"Completed. (Click here to show output {arrow})") # Show Reed-Solomon stats if it fixed corrupted bytes if mode == "decrypt" and reedsoloFixed: tmp = "s" if reedsoloFixed != 1 else "" statusString.set(f"Completed with {reedsoloFixed} byte{tmp}" + f" fixed. (Click here to show output {arrow})") else: if kept == "modified": statusString.set(strings[7]) elif kept == "corrupted": statusString.set(strings[6]) else: statusString.set(strings[8]) status.config(cursor="hand2") # A little hack to prevent reference nonsense output = "".join([i for i in outputFile]) # Bind the output file to the status label 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 resetUI() inputFile = "" outputFile = "" allFiles = [] onlyFolders = [] onlyFiles = [] working = False