Exemple #1
0
def decrypt_device_line(patient_id, key, data: bytes) -> bytes:
    """ Config is expected to be 3 colon separated values.
        value 1 is the symmetric key, encrypted with the patient's public key.
        value 2 is the initialization vector for the AES CBC cipher.
        value 3 is the config, encrypted using AES CBC, with the provided key and iv. """
    iv, data = data.split(b":")
    iv = decode_base64(iv)  # handle non-ascii encoding garbage...
    data = decode_base64(data)
    if not data:
        raise InvalidData()
    if not iv:
        raise InvalidIV()
    try:
        decipherer = AES.new(key, mode=AES.MODE_CBC, IV=iv)
        decrypted = decipherer.decrypt(data)
    except Exception:
        if iv is None:
            len_iv = "None"
        else:
            len_iv = len(iv)
        if data is None:
            len_data = "None"
        else:
            len_data = len(data)
        if key is None:
            len_key = "None"
        else:
            len_key = len(key)
        # these print statements cause problems in getting encryption errors because the print
        # statement will print to an ascii formatted log file on the server, which causes
        # ascii encoding error.  Enable them for debugging only. (leave uncommented for Sentry.)
        # print("length iv: %s, length data: %s, length key: %s" % (len_iv, len_data, len_key))
        # print('%s %s %s' % (patient_id, key, data))
        raise

    # PKCS5 Padding: The last byte of the byte-string contains the number of bytes at the end of the
    # bytestring that are padding.  As string slicing in python are a copy operation we will
    # detect the fast-path case of no change so that we can skip it
    num_padding_bytes = decrypted[-1]
    if num_padding_bytes:
        decrypted = decrypted[0:-num_padding_bytes]

    return decrypted
 def decode(self):
     return decode_base64(self.contents)
Exemple #3
0
def extract_aes_key(file_data: List[bytes], participant: Participant,
                    private_key_cipher, original_data: bytes) -> bytes:
    # The following code is ... strange because of an unfortunate design design decision made
    # quite some time ago: the decryption key is encoded as base64 twice, once wrapping the
    # output of the RSA encryption, and once wrapping the AES decryption key.  This happened
    # because I was not an experienced developer at the time, python2's unified string-bytes
    # class didn't exactly help, and java io is... java io.

    def create_decryption_key_error(an_traceback):
        # helper function with local variable access.
        # do not refactor to include raising the error in this function, that obfuscates the source.
        if STORE_DECRYPTION_KEY_ERRORS:

            DecryptionKeyError.objects.create(
                file_path=request.values['file_name'],
                contents=original_data.decode(),
                traceback=an_traceback,
                participant=participant,
            )

    try:
        key_base64_raw: bytes = file_data[0]
        # print(f"key_base64_raw: {key_base64_raw}")
    except IndexError:
        # probably not reachable due to test for emptiness prior in code; keep just in case...
        create_decryption_key_error(traceback.format_exc())
        raise DecryptionKeyInvalidError("There was no decryption key.")

    # Test that every "character" (they are 8 bit bytes) in the byte-string of the raw key is
    # a valid url-safe base64 character, this will cut out certain junk files too.
    for c in key_base64_raw:
        if c not in URLSAFE_BASE64_CHARACTERS:
            # need a stack trace....
            try:
                raise DecryptionKeyInvalidError(
                    f"Decryption key not base64 encoded: {key_base64_raw}")
            except DecryptionKeyInvalidError:
                create_decryption_key_error(traceback.format_exc())
                raise

    # handle the various cases that can occur when extracting from base64.
    try:
        decoded_key: bytes = decode_base64(key_base64_raw)
        # print(f"decoded_key: {decoded_key}")
    except (TypeError, PaddingException,
            Base64LengthException) as decode_error:
        create_decryption_key_error(traceback.format_exc())
        raise DecryptionKeyInvalidError(
            f"Invalid decryption key: {decode_error}")

    try:
        base64_key = private_key_cipher.decrypt(decoded_key)
        # print(f"base64_key: {len(base64_key)} {base64_key}")
        decrypted_key = decode_base64(base64_key)
        # print(f"decrypted_key: {len(decrypted_key)} {decrypted_key}")
        if not decrypted_key:
            raise TypeError(f"decoded key was '{decrypted_key}'")
    except (TypeError, IndexError, PaddingException,
            Base64LengthException) as decr_error:
        create_decryption_key_error(traceback.format_exc())
        raise DecryptionKeyInvalidError(
            f"Invalid decryption key: {decr_error}")

    # If the decoded bits of the key is not exactly 128 bits (16 bytes) that probably means that
    # the RSA encryption failed - this occurs when the first byte of the encrypted blob is all
    # zeros.  Apps require an update to solve this (in a future rewrite we should use a padding
    # algorithm).
    if len(decrypted_key) != 16:
        # print(len(decrypted_key))
        # need a stack trace....
        try:
            raise DecryptionKeyInvalidError(
                f"Decryption key not 128 bits: {decrypted_key}")
        except DecryptionKeyInvalidError:
            create_decryption_key_error(traceback.format_exc())
            raise

    return decrypted_key
Exemple #4
0
def decrypt_device_file(patient_id, original_data: bytes, private_key_cipher,
                        user) -> bytes:
    """ Runs the line-by-line decryption of a file encrypted by a device.
    This function is a special handler for iOS file uploads. """
    def create_line_error_db_entry(error_type):
        # declaring this inside decrypt device file to access its function-global variables
        if IS_STAGING:
            LineEncryptionError.objects.create(
                type=error_type,
                base64_decryption_key=private_key_cipher.decrypt(decoded_key),
                line=encode_base64(line),
                prev_line=encode_base64(file_data[i - 1] if i > 0 else ''),
                next_line=encode_base64(file_data[i +
                                                  1] if i < len(file_data) -
                                        1 else ''),
                participant=user,
            )

    def create_decryption_key_error(an_traceback):
        DecryptionKeyError.objects.create(
            file_path=request.values['file_name'],
            contents=original_data.decode(),
            traceback=an_traceback,
            participant=user,
        )

    bad_lines = []
    error_types = []
    error_count = 0
    good_lines = []
    file_data = [line for line in original_data.split(b'\n') if line != b""]

    if not file_data:
        raise HandledError(
            "The file had no data in it.  Return 200 to delete file from device."
        )

    # The following code is strange because of an unfortunate design design decision made quite
    # some time ago: the decryption key is encoded as base64 twice, once wrapping the output of the
    # RSA encryption, and once wrapping the AES decryption key.
    # The second of the two except blocks likely means that the device failed to write the encryption
    # key as the first line of the file, but it may be a valid (but undecryptable) line of the  file.
    try:
        decoded_key = decode_base64(file_data[0])
    except (TypeError, IndexError, PaddingException) as decode_error:
        create_decryption_key_error(traceback.format_exc())
        raise DecryptionKeyInvalidError("invalid decryption key. %s" %
                                        decode_error)

    try:
        base64_key = private_key_cipher.decrypt(decoded_key)
        decrypted_key = decode_base64(base64_key)
    except (TypeError, IndexError, PaddingException) as decr_error:
        create_decryption_key_error(traceback.format_exc())
        raise DecryptionKeyInvalidError("invalid decryption key. %s" %
                                        decr_error)

    for i, line in enumerate(file_data):
        # we need to skip the first line (the decryption key), but need real index values in i
        if i == 0:
            continue

        if line is None:
            # this case causes weird behavior inside decrypt_device_line, so we test for it instead.
            error_count += 1
            create_line_error_db_entry(LineEncryptionError.LINE_IS_NONE)
            error_types.append(LineEncryptionError.LINE_IS_NONE)
            bad_lines.append(line)
            print("encountered empty line of data, ignoring.")
            continue

        try:
            good_lines.append(
                decrypt_device_line(patient_id, decrypted_key, line))
        except Exception as error_orig:
            error_string = str(error_orig)
            error_count += 1

            error_message = "There was an error in user decryption: "
            if isinstance(error_string, IndexError):
                error_message += "Something is wrong with data padding:\n\tline: %s" % line
                log_error(error_string, error_message)
                create_line_error_db_entry(LineEncryptionError.PADDING_ERROR)
                error_types.append(LineEncryptionError.PADDING_ERROR)
                bad_lines.append(line)
                continue

            if isinstance(error_string, TypeError) and decrypted_key is None:
                error_message += "The key was empty:\n\tline: %s" % line
                log_error(error_string, error_message)
                create_line_error_db_entry(LineEncryptionError.EMPTY_KEY)
                error_types.append(LineEncryptionError.EMPTY_KEY)
                bad_lines.append(line)
                continue

            ################### skip these errors ##############################
            if "unpack" in error_string:
                error_message += "malformed line of config, dropping it and continuing."
                log_error(error_string, error_message)
                create_line_error_db_entry(
                    LineEncryptionError.MALFORMED_CONFIG)
                error_types.append(LineEncryptionError.MALFORMED_CONFIG)
                bad_lines.append(line)
                #the config is not colon separated correctly, this is a single
                # line error, we can just drop it.
                # implies an interrupted write operation (or read)
                continue

            if "Input strings must be a multiple of 16 in length" in error_string:
                error_message += "Line was of incorrect length, dropping it and continuing."
                log_error(error_string, error_message)
                create_line_error_db_entry(LineEncryptionError.INVALID_LENGTH)
                error_types.append(LineEncryptionError.INVALID_LENGTH)
                bad_lines.append(line)
                continue

            if isinstance(error_string, InvalidData):
                error_message += "Line contained no data, skipping: " + str(
                    line)
                log_error(error_string, error_message)
                create_line_error_db_entry(LineEncryptionError.LINE_EMPTY)
                error_types.append(LineEncryptionError.LINE_EMPTY)
                bad_lines.append(line)
                continue

            if isinstance(error_string, InvalidIV):
                error_message += "Line contained no iv, skipping: " + str(line)
                log_error(error_string, error_message)
                create_line_error_db_entry(LineEncryptionError.IV_MISSING)
                error_types.append(LineEncryptionError.IV_MISSING)
                bad_lines.append(line)
                continue

            ##################### flip out on these errors #####################
            if 'AES key' in error_string:
                error_message += "AES key has bad length."
                create_line_error_db_entry(
                    LineEncryptionError.AES_KEY_BAD_LENGTH)
                error_types.append(LineEncryptionError.AES_KEY_BAD_LENGTH)
                bad_lines.append(line)
            elif 'IV must be' in error_string:
                error_message += "iv has bad length."
                create_line_error_db_entry(LineEncryptionError.IV_BAD_LENGTH)
                error_types.append(LineEncryptionError.IV_BAD_LENGTH)
                bad_lines.append(line)
            elif 'Incorrect padding' in error_string:
                error_message += "base64 padding error, config is truncated."
                create_line_error_db_entry(LineEncryptionError.MP4_PADDING)
                error_types.append(LineEncryptionError.MP4_PADDING)
                bad_lines.append(line)
                # this is only seen in mp4 files. possibilities:
                #  upload during write operation.
                #  broken base64 conversion in the app
                #  some unanticipated error in the file upload
            else:
                # If none of the above errors happened, raise the error raw
                raise
            raise HandledError(error_message)
            # if any of them did happen, raise a HandledError to cease execution.

    if error_count:
        EncryptionErrorMetadata.objects.create(
            file_name=request.values['file_name'],
            total_lines=len(file_data),
            number_errors=error_count,
            # generator comprehension:
            error_lines=json.dumps((str(line for line in bad_lines))),
            error_types=json.dumps(error_types),
            participant=user,
        )

    # join should be rather well optimized and not cause O(n^2) total memory copies
    return b"\n".join(good_lines)