Exemple #1
0
    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 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)
Exemple #3
0
    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)
Exemple #4
0
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
Exemple #5
0
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
Exemple #6
0
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
Exemple #9
0
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))
Exemple #10
0
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)
    )
Exemple #11
0
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)
Exemple #12
0
    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)
Exemple #14
0
    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)
Exemple #15
0
    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)
Exemple #16
0
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
Exemple #18
0
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
Exemple #19
0
 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
Exemple #20
0
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