2023-12-21 18:14:39.24497+00
Firstly, I installed and import the forge.js module. The module can be copied or downloaded from node-forge npm by selecting the dist/forge.min.js file and moving the content into the assets/vendor/forge.js (could be named anything).
Next, import the forge object in your assets/js/app.js file:
import forge from "../vendor/forge";
crypto_web.ex
defmodule FileStorage.Crypto.Web do
@moduledoc """
Cryptography methods used for encryption and signatures on files downloaded from web form
"""
@key_encryption_padding_options [{:encrypt, false}, {:padding, :pkcs_padding}]
@aes_cipher :aes_256_ctr
@hash_cipher :sha512
@pbkdf2_interations 310 # Unsure if 310_000 is required since this is not password. Therefore, no rainbow attack.
@private_key_pem System.fetch_env!("PRIVATE_KEY_PEM")
|> to_string()
|> String.replace("\\n", "\n")
@spec gen_ecdh_keypair(atom()) :: {binary(), tuple()}
def gen_ecdh_keypair(named_curve) when is_atom(named_curve) do
private_key = :public_key.generate_key({:namedCurve, named_curve})
public_key = {{:ECPoint, elem(private_key, 4)}, elem(private_key, 3)}
public_entry = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
{public_entry |> elem(1), private_key}
end
@spec compute_shared_secret!(binary(), tuple()) :: binary()
def compute_shared_secret!(public_key_der, private_key)
when is_binary(public_key_der) and is_tuple(private_key) do
public_key_point =
:public_key.pem_entry_decode({:SubjectPublicKeyInfo, public_key_der, :not_encrypted})
|> elem(0)
:public_key.compute_key(public_key_point, private_key)
end
@spec key_derivation!(binary()) :: binary()
def key_derivation!(shared_secret) when is_binary(shared_secret) do
key_part = binary_part(shared_secret, 0, 32)
salt_part = binary_part(shared_secret, 32, 32)
Pbkdf2.Base.hash_password(key_part, salt_part, rounds: @pbkdf2_interations, digest: :sha256, format: :hex)
|> Base.decode16!(case: :lower)
end
@spec sign_key!(binary()) :: binary()
def sign_key!(public_key_pem) when is_binary(public_key_pem) do
{:ok, private_key} = decode_private_pem(@private_key_pem)
:public_key.sign(public_key_pem, :sha256, private_key)
end
# ...
end
form_component.ex
defmodule FileStorageWeb.FileLive.FormComponent do
use FileStorageWeb, :live_component
alias FileStorage.Storages
alias FileStorage.Accounts
alias FileStorageWeb.MultiSelectComponent
alias FileStorageWeb.FileLive.Filewriter
alias FileStorage.Crypto.Web, as: Crypto
# ...
def render(assigns) do
# ...
<.live_file_input upload={@uploads.file} class="hidden" />
<%= if @folder do %>
<input
id="dropzone-file"
type="file"
class="hidden"
webkitdirectory={@folder}
phx-hook="Folder"
name={@edh}
/>
<% else %>
<input id="dropzone-file" name={@edh} type="file" class="hidden" phx-hook="File" />
<% end %>
# ...
end
def update(%{file: file, current_user: user, action: action} = assigns, socket) do
{public_key_der, private_key} = Crypto.gen_ecdh_keypair
(:secp521r1)
base64_public_key_der = public_key_der |> Base.encode64()
base64_signature = Crypto.sign_key!(base64_public_key_der) |> Base.encode64()
{:ok,
socket
|> assign(assigns)
|> assign(:current_user, updated_user)
|> assign(:folder, action == :folder || file.is_folder)
|> assign(:loading, false)
|> assign(:edh, base64_signature <> "," <> base64_public_key_der)
|> assign(:edh_private, private_key)
|> assign(:shared_key, nil)
# ...
}
end
def handle_event("DHexchange", %{"publicKey" => base64_public_key_der}, socket) do
shared_key =
Crypto.compute_shared_secret!(
Base.decode64!(base64_public_key_der),
socket.assigns.edh_private
)
|> Crypto.key_derivation!()
# TODO: Secret storage? or often updated
{:noreply, assign(socket, :shared_key, shared_key) |> assign(:edh_private, nil)}
end
app.js
// ...
import { encryptContentToBlob, generateSymmetricKey } from "./utils";
let hooks = {};
hooks.Folder = {
async mounted() {
const sharedKey = await generateSymmetricKey(this);
// ...
},
};
hooks.File = {
async mounted() {
const sharedKey = await generateSymmetricKey(this);
// ...
},
};
// ...
utils.js
// ...
/**
* TODO: Should be stored better ;(
* @returns public signing key
*/
const getPublicSignKey = async () => {
const pemDecoded = forge.pem.decode("-----BEGIN PUBLIC KEY-----\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAA/OGbMbh8AFZep/vYM1o7BunL48x\nZuciAFDQ551pTT7SCffLL9xhCelJhJhM1ywDtDU5KVYKYWAlEX8yM3B7U/cAZr77\nwwrHkoW+orROTAHnjiMuRq5GfHXgZoAbQoU8EYbfQlIenAWh4FocWwceqc6XUDhK\n2xsQG6ZoEL477ssRGmc=\n-----END PUBLIC KEY-----")[0]
return await crypto.subtle.importKey("spki", stringToArrayBuffer(pemDecoded.body), {name: "ECDSA", namedCurve: "P-521"}, false, ["verify"])
}
/**
* Checks the signature against the data
* @param {string} signature
* @param {string} data -> a DER encoded EC public key
* @returns verification of the signature represented by a boolean
*/
const verifySignature = async (signature, data) => {
const publicKey = await getPublicSignKey()
return await crypto.subtle.verify({name: "ECDSA", hash: "SHA-256"}, publicKey, signature, data)
}
/**
* Derives a key from a shared secret using PBKDF2 (a key derivation function)
* @param {string} sharedSecret
* @returns a symmetric key
*/
const generateSymmetricKeyFromSharedSecret = async (sharedSecret) => {
const keyPart = sharedSecret.slice(0, 32);
const saltPart = sharedSecret.slice(32, 64);
const importedKey = await crypto.subtle.importKey("raw", keyPart, "PBKDF2", false, ["deriveBits"]);
return new Uint8Array(await crypto.subtle.deriveBits({name: "PBKDF2", hash: "SHA-256", salt: saltPart, iterations: 310}, importedKey, 256));
}
/**
* Verifies the DER encoded publicKey with the signature
* Generates its own EDH-part and calculates the shared secret => key derivation
* @param {string} signature
* @param {string} publicKeyDer
* @returns symmetricKey derviced from the sharedSecret and its publicKey part
*/
const verifyAndGenerateSymmetricKey = async (signature, publicKeyDer) => {
try {
const signatureBuffer = stringToArrayBuffer(window.atob(signature))
const publicKeyDerBuffer = stringToArrayBuffer(window.atob(publicKeyDer))
if(verifySignature(signatureBuffer, publicKeyDerBuffer)){
const importedPublicKey = await crypto.subtle.importKey("spki", publicKeyDerBuffer, {name: "ECDH", namedCurve: "P-521"}, true, ["deriveBits"])
const browserKeyPair = await crypto.subtle.generateKey({name: "ECDH", namedCurve: "P-521"}, true, ["deriveBits", "deriveKey"])
const sharedSecret = await crypto.subtle.deriveBits({ name:"ECDH", public: importedPublicKey }, browserKeyPair.privateKey, 512)
return {key: await generateSymmetricKeyFromSharedSecret(sharedSecret), publicKey: new Uint8Array(await crypto.subtle.exportKey('spki', browserKeyPair.publicKey))}
} else
return {error: "Could not verify signature of key"}
} catch {
return {error: "Issue occured trying to generate shared secret"}
}
}
/**
* Fetches the name of the mounted element, which should contain the server's key part and its signature
* Generates its own EDH-part and calculates the shared secret => key derivation
* Sends its public EDH-part to the server
* @param {HTMLInputElement} mountEl
* @returns symmetricKey derviced from the sharedSecret
*/
export const generateSymmetricKey = async (mountEl) => {
const nameSplit = mountEl.el.name.split(",")
const sharedSecretAndDHPart = await verifyAndGenerateSymmetricKey(nameSplit[0], nameSplit[1])
try {
if(sharedSecretAndDHPart.error == undefined){
await mountEl.pushEventTo("1", "DHexchange", {publicKey: window.btoa(String.fromCharCode.apply(null, sharedSecretAndDHPart.publicKey))})
return sharedSecretAndDHPart.key
} else {
await mountEl.pushEvent("error", sharedSecretAndDHPart)
return undefined
}
} catch(e) {
await mountEl.pushEvent("error", "Could not transfer key")
return undefined
}
}
const stringToArrayBuffer = (byteString) => {
const byteArray = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
byteArray[i] = byteString.charCodeAt(i);
}
return byteArray;
}
// ...
app.js
// ...
import JSZip from "../vendor/jszip";
import { encryptContentToBlob, generateSymmetricKey } from "./utils";
let hooks = {};
hooks.Folder = {
async mounted() {
const sharedKey = await generateSymmetricKey(this);
this.el.addEventListener("input", async (e) => {
e.preventDefault();
this.pushEventTo("1", "loading")
const folderName = e.target.files[0].webkitRelativePath.split("/")[0];
const zip = new JSZip();
Array.from(e.target.files).forEach((file) => {
zip.file(file.webkitRelativePath || file.name, file, { binary: true });
});
const zipBlob = await zip.generateAsync({ type: "blob" });
const encryptedBlob = await encryptContentToBlob(zipBlob, folderName, sharedKey);
this.upload("file", [encryptedBlob]);
});
},
};
hooks.File = {
async mounted() {
const sharedKey = await generateSymmetricKey(this);
this.el.addEventListener("input", async (e) => {
e.preventDefault();
await this.pushEventTo("1", "loading")
const file = Array.from(e.target.files)[0];
const encryptedBlob = await encryptContentToBlob(file, file.name, sharedKey);
this.upload("file", [encryptedBlob]);
});
},
};
// ...
utils.js
import forge from "../vendor/forge";
/**
* Converts a Blob and a filename to a File type
* @param {Blob} blob
* @param {string} fileName
* @returns {File} of the Blob and filename
*/
function blobToFile(blob, fileName) {
// Get the mime type of the blob
var mimeType = blob.type;
// Create a new file object using the blob and filename
var file = new File([blob], fileName, { type: mimeType });
return file;
}
/**
* Encrypts a file using AES in CTR mode
* SHA512 hashes the ciphertext
* @param {File} file
* @param {string} key
* @param {string} iv
* @returns the ciphertext with its hash
*/
const encryptAesCtrAndSha512 = async (file, key, iv) => {
return new Promise(async (res, _rej) => {
const cipher = forge.cipher.createCipher("AES-CTR", key);
const md = forge.hmac.create();
cipher.start({
iv: iv,
});
md.start("sha512", key)
const fileReader = file.stream().getReader();
const encryptedData = [];
let finished = false;
let i = 0;
while (!finished) {
const { value, done } = await fileReader.read();
finished = done;
if (!finished) {
if (i % 5 == 0) await refreshUi();
cipher.update(forge.util.createBuffer(value));
const encryptedBytes = cipher.output.getBytes();
md.update(encryptedBytes);
bytes = window.btoa(encryptedBytes);
encryptedData.push(bytes);
}
i++;
}
cipher.finish();
res({
encrypted: encryptedData.join(","),
hash: md.digest().bytes(),
});
});
};
/**
* Encrypts a file using the sharedKey
* @param {File} file
* @param {string} sharedKey
* @returns Object containing the ciphertext with its IV and the hash of the ciphertext
*/
const encryptFile = async (file, sharedKey) => {
return new Promise(async (res, rej) => {
forge.options.usePureJavaScript = true;
const iv = forge.random.getBytesSync(16);
const { encrypted, hash } = await encryptAesCtrAndSha512(file, sharedKey, iv);
res({
cipherText: encrypted,
hash: window.btoa(hash),
iv: window.btoa(iv),
});
});
};
/**
* Encrypts a file
* Converts the filename to contain the hash and iv
* @param {File} file
* @param {string} filename
* @param {Uint8Array} sharedKey
* @returns {File} the encrypted file
*/
export const encryptContentToBlob = async (file, filename, sharedKey) => {
const encryptedFile = await encryptFile(file, String.fromCharCode.apply(null, sharedKey));
const encryptedBlob = new Blob([encryptedFile.cipherText], {
type: file.type,
});
const combinedFilename =
encryptedFile.hash + "," + encryptedFile.iv + "," + filename;
if (combinedFilename.length - filename.length >= 100)
return blobToFile(encryptedBlob, combinedFilename);
else return blobToFile(file, filename);
};
// ...
crypto_web.ex
defmodule FileStorage.Crypto.Web do
@moduledoc """
Cryptography methods used for encryption and signatures on files downloaded from web form
"""
@key_encryption_padding_options [{:encrypt, false}, {:padding, :pkcs_padding}]
@aes_cipher :aes_256_ctr
@hash_cipher :sha512
@pbkdf2_interations 310 # Unsure if 310_000 is required since this is not password. Therefore, no rainbow attack.
@private_key_pem System.fetch_env!("PRIVATE_KEY_PEM")
|> to_string()
|> String.replace("\\n", "\n")
# ...
@spec mac_init(binary()) :: {:ok, reference()}
def mac_init(key) when is_binary(key) do
{:ok, :crypto.mac_init(:hmac, @hash_cipher, key)}
end
@spec mac_chunk(reference(), binary()) :: {:ok, reference()}
def mac_chunk(state, chunk) when is_binary(chunk) and is_reference(state) do
{:ok, :crypto.mac_update(state, chunk)}
end
@spec mac_digest(reference()) :: {:ok, binary()}
def mac_digest(state) when is_reference(state) do
{:ok, :crypto.mac_final(state)}
end
@spec mac_equals!(binary(), binary()) :: boolean()
def mac_equals!(mac1, mac2) when is_binary(mac1) and is_binary(mac2) do
:crypto.hash_equals(mac1, mac2)
end
@spec decrypt_init(binary(), binary()) ::
{:ok, reference()}
def decrypt_init(key, iv) when is_binary(key) and is_binary(iv) do
{:ok, :crypto.crypto_init(@aes_cipher, key, iv, @key_encryption_padding_options)}
end
@spec decrypt_chunk(reference(), binary()) :: {:ok, binary()} | {:error, :aes_decrypt_error}
def decrypt_chunk(decrypter_state, cipher_chunk)
when is_reference(decrypter_state) and is_binary(cipher_chunk) do
{:ok, :crypto.crypto_update(decrypter_state, cipher_chunk)}
rescue
_ -> {:error, :aes_decrypt_error}
end
@spec decrypt_finalize(reference()) :: {:ok, binary()} | {:error, :aes_decrypt_error}
def decrypt_finalize(decrypter_state) when is_reference(decrypter_state) do
{:ok, :crypto.crypto_final(decrypter_state)}
rescue
_ -> {:error, :aes_decrypt_error}
end
@spec decode_private_pem(binary()) :: {:ok, binary()}
defp decode_private_pem(pem) when is_binary(pem) do
{:ok,
pem
|> :public_key.pem_decode()
|> List.first()
|> :public_key.pem_entry_decode()}
end
end
crypto_s3.ex
defmodule FileStorage.Crypto.S3 do
@moduledoc """
Cryptography methods used for encryption and signatures on files uploaded to S3
"""
@key_encryption_padding_options {:padding, :pkcs_padding}
@aes_mode :aes_256_ctr
@hash_cipher :sha512
@spec random_bytes(number()) :: binary()
def random_bytes(length) do
:crypto.strong_rand_bytes(length)
end
@spec encrypt_key!(binary(), binary()) :: binary()
@spec encrypt_key!({binary(), binary()}, binary(), binary()) :: binary()
def encrypt_key!(key, key_iv) when is_binary(key) and is_binary(key_iv) do
:crypto.crypto_one_time(@aes_mode, System.fetch_env!("KEK"), key_iv, key, [
{:encrypt, true},
@key_encryption_padding_options
])
end
def encrypt_key!({key, key_iv}, encrypted_key, encrypted_key_iv)
when is_binary(key) and is_binary(key_iv) and is_binary(encrypted_key) and
is_binary(encrypted_key_iv) do
decrypted_key = decrypt_key!(encrypted_key, encrypted_key_iv)
:crypto.crypto_one_time(@aes_mode, decrypted_key, key_iv, key, [
{:encrypt, true},
@key_encryption_padding_options
])
end
@spec decrypt_key!(binary(), binary()) :: binary()
@spec decrypt_key!({binary(), binary()}, binary(), binary()) :: binary()
def decrypt_key!(key_cipher, key_iv) when is_binary(key_cipher) and is_binary(key_iv) do
:crypto.crypto_one_time(@aes_mode, System.fetch_env!("KEK"), key_iv, key_cipher, [
{:encrypt, false},
@key_encryption_padding_options
])
end
def decrypt_key!({key_cipher, key_iv}, encrypted_key, encrypted_key_iv)
when is_binary(key_cipher) and is_binary(key_iv) and is_binary(encrypted_key) and
is_binary(encrypted_key_iv) do
decrypted_key = decrypt_key!(encrypted_key, encrypted_key_iv)
:crypto.crypto_one_time(@aes_mode, decrypted_key, key_iv, key_cipher, [
{:encrypt, false},
@key_encryption_padding_options
])
end
@spec encrypt_init(binary(), binary(), boolean()) ::
{:ok, reference()}
def encrypt_init(key, iv, is_encrypt)
when is_binary(key) and is_binary(iv) and is_boolean(is_encrypt) do
{:ok,
:crypto.crypto_init(@aes_mode, key, iv, [
{:encrypt, is_encrypt},
@key_encryption_padding_options
])}
end
@spec encrypt_chunk(reference(), binary()) :: {:ok, binary()} | {:error, :aes_encrypt_error}
def encrypt_chunk(encrypter_state, cipher_chunk)
when is_reference(encrypter_state) and is_binary(cipher_chunk) do
{:ok, :crypto.crypto_update(encrypter_state, cipher_chunk)}
rescue
_ -> {:error, :aes_encrypt_error}
end
@spec encrypt_finalize(reference()) :: {:ok, binary()} | {:error, :aes_encrypt_error}
def encrypt_finalize(encrypter_state) when is_reference(encrypter_state) do
{:ok, :crypto.crypto_final(encrypter_state)}
rescue
_ -> {:error, :aes_encrypt_error}
end
@spec mac_init(binary()) :: {:ok, reference()}
def mac_init(key) when is_binary(key) do
{:ok, :crypto.mac_init(:hmac, @hash_cipher, key)}
rescue
_ -> {:error, :mac_init_error}
end
@spec mac_chunk(reference(), binary()) :: {:ok, reference()}
def mac_chunk(state, chunk) when is_binary(chunk) and is_reference(state) do
{:ok, :crypto.mac_update(state, chunk)}
rescue
_ -> {:error, :mac_chunk_error}
end
@spec mac_digest(reference()) :: {:ok, binary()}
def mac_digest(state) when is_reference(state) do
{:ok, :crypto.mac_final(state)}
rescue
_ -> {:error, :mac_digest_error}
end
@spec mac!(binary(), binary()) :: binary()
def mac!(key, data) when is_binary(key) and is_binary(data) do
:crypto.mac(:hmac, @hash_cipher, key, data)
end
@spec mac_equals!(binary(), binary()) :: boolean()
def mac_equals!(mac1, mac2) when is_binary(mac1) and is_binary(mac2) do
:crypto.hash_equals(mac1, mac2)
end
end
form_component.ex
allow_upload(:file,
accept: :any,
max_entries: 1,
max_file_size: updated_user.max_size,
# 65536 base64 encoded (plus 1 for comma)
chunk_size: 87_385,
writer: fn _name, %{client_name: name}, socket ->
{mac, iv, _} = filename_to_meta(name)
{Filewriter, mac: mac, key: socket.assigns.shared_key, iv: iv, parent: self()}
end,
auto_upload: true)
filewriter.ex
# https://github.com/phoenixframework/phoenix_live_view/blob/main/lib/phoenix_live_view/upload_tmp_file_writer.ex
defmodule FileStorageWeb.FileLive.Filewriter do
@moduledoc """
FileWriter implementation
Decrypts the file received from the client
Re-encrypts the file with a new symmetric key
"""
@behaviour Phoenix.LiveView.UploadWriter
alias FileStorage.Crypto.Web, as: Crypto
alias FileStorage.Crypto.S3, as: CryptoS3
@spec init(keyword()) ::
{:error, atom}
| {:no_tmp, [binary]}
| {:ok,
%{
file: pid | {:file_descriptor, atom, any},
mac: any,
path: <<_::32, _::_*8>>,
mac_state: reference(),
state: reference
}}
| {:too_many_attempts, binary, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}
def init(opts) do
key = Keyword.fetch!(opts, :key)
parent = Keyword.fetch!(opts, :parent)
encrypt_key = CryptoS3.random_bytes(32)
encrypt_iv = CryptoS3.random_bytes(16)
if key != nil do
iv = Keyword.fetch!(opts, :iv)
mac = Keyword.fetch!(opts, :mac)
with {:ok, path} <- Plug.Upload.random_file("live_view_upload"),
{:ok, file} <- File.open(path, [:binary, :write]),
{:ok, decrypt_state} <- Crypto.decrypt_init(key, iv),
{:ok, mac_state} <- Crypto.mac_init(key),
{:ok, encrypt_state} <- CryptoS3.encrypt_init(encrypt_key, encrypt_iv, true),
{:ok, encrypt_mac_state} <- CryptoS3.mac_init(encrypt_key) do
{:ok,
%{
path: path,
file: file,
mac: mac,
decrypt_state: decrypt_state,
mac_state: mac_state,
encrypt_state: encrypt_state,
encrypt_mac_state: encrypt_mac_state,
encrypt_key: encrypt_key,
encrypt_iv: encrypt_iv,
overflow: "",
parent: parent
}}
end
else
# Defaults to no encryption, could be attack vector I guess ;(
with {:ok, path} <- Plug.Upload.random_file("live_view_upload"),
{:ok, file} <- File.open(path, [:binary, :write]),
{:ok, encrypt_state} <- CryptoS3.encrypt_init(encrypt_key, encrypt_iv, true),
{:ok, encrypt_mac_state} <- CryptoS3.mac_init(key) do
{:ok,
%{
path: path,
file: file,
default: true,
parent: parent,
encrypt_state: encrypt_state,
encrypt_mac_state: encrypt_mac_state,
encrypt_key: encrypt_key,
encrypt_iv: encrypt_iv
}}
end
end
end
@spec meta(map()) :: map()
def meta(state) do
%{
path: state.path,
key: state.encrypt_key,
iv: state.encrypt_iv,
mac: state.encrypt_mac_state
}
end
@spec write_chunk(binary(), map()) :: {:ok, map()} | {:error, any()}
def write_chunk(data, %{default: true} = state) do
case encrypt_and_write_to_file(state, {:ok, data}) do
{:ok, new_encrypt_mac_state} ->
{:ok, Map.put(state, :encrypt_mac_state, new_encrypt_mac_state)}
{:error, reason} ->
notify_error_parent({:error, reason}, state.parent)
{:error, reason}
end
end
def write_chunk(encrypted_data, state) do
{decoded, overflow} = decode64!(state.overflow <> encrypted_data)
decrypt_result = Crypto.decrypt_chunk(state.decrypt_state, decoded)
{:ok, state} =
if(byte_size(overflow) > 0, do: Map.put(state, :overflow, overflow), else: state)
|> update_mac_state(decoded)
case encrypt_and_write_to_file(state, decrypt_result) do
{:ok, new_encrypt_mac_state} ->
{:ok, Map.put(state, :encrypt_mac_state, new_encrypt_mac_state)}
{:error, reason} ->
notify_error_parent({:error, reason}, state.parent)
{:error, reason}
end
end
@spec close(map(), :done | :cancel | {:error, term}) :: {:ok, map()} | {:error, any()}
def close(state, reason) do
case reason do
:cancel -> {:error, :canceled}
:done -> close(state)
{:error, term} -> {:error, term}
end
end
defp close(%{default: true} = state) do
case File.close(state.file) do
:ok ->
{:ok, state}
{:error, reason} ->
notify_error_parent({:error, reason}, state.parent)
{:error, reason}
end
end
defp close(state) do
# Write to file overflow in state
{:ok, state} =
if byte_size(state.overflow) > 0, do: write_chunk("", state), else: {:ok, state}
# Write to file decrypted bytes in decryptor state
write_result = encrypt_and_write_to_file(state, Crypto.decrypt_finalize(state.decrypt_state))
if elem(write_result, 0) == :ok and
Crypto.mac_equals!(elem(Crypto.mac_digest(state.mac_state), 1), state.mac) do
case File.close(state.file) do
:ok ->
{:ok,
Map.put(
state,
:encrypt_mac_state,
elem(write_result, 1) |> CryptoS3.mac_digest() |> elem(1)
)}
{:error, reason} ->
notify_error_parent({:error, reason}, state.parent)
{:error, reason}
end
else
File.close(state.file)
if elem(write_result, 0) == :ok do
notify_error_parent({:error, "MACs did not match"}, state.parent)
{:error, "MACs did not match"}
else
notify_error_parent(write_result, state.parent)
write_result
end
end
end
@spec encrypt_and_write_to_file(map(), {:ok, binary()} | {:error, term()}) ::
{:ok, reference()} | {:error, term()}
defp encrypt_and_write_to_file(state, {:ok, data}) when is_binary(data) do
case CryptoS3.encrypt_chunk(state.encrypt_state, data) do
{:ok, encrypted_data} ->
write_to_file(state.file, encrypted_data)
|> mac_chunk(state, encrypted_data)
{:error, reason} ->
{:error, reason}
end
end
defp encrypt_and_write_to_file(_state, {:error, reason}) do
{:error, reason}
end
@spec write_to_file(:file.io_device(), binary()) ::
:ok | {:error, any()}
defp write_to_file(file, data) when is_binary(data) do
IO.binwrite(file, data)
end
@spec decode64!(binary()) :: {binary(), binary()}
defp decode64!(data) do
check_base64_data = String.trim_leading(data, ",") |> String.split(",")
{chunk, overflow} =
cond do
# Unlikely this will ever happen
length(check_base64_data) > 2 ->
{first_chunk, rest_list} = List.pop_at(check_base64_data, 0)
{first_chunk, Enum.join(rest_list, "")}
length(check_base64_data) == 2 and byte_size(List.last(check_base64_data)) > 0 ->
[head | tail] = check_base64_data
{head, tail}
true ->
{List.first(check_base64_data), ""}
end
{Base.decode64!(chunk), overflow}
end
@spec update_mac_state(map(), binary()) :: {:ok, map()}
defp update_mac_state(state, data) do
{:ok, new_mac_state} = Crypto.mac_chunk(state.mac_state, data)
{:ok, Map.put(state, :mac_state, new_mac_state)}
end
defp notify_error_parent({:error, reason}, parent),
do: send(parent, {__MODULE__, {:error, reason}})
@spec mac_chunk(:ok | {:error, term()}, map(), binary()) ::
{:ok, reference()} | {:error, term()}
defp mac_chunk(:ok, state, data) when is_binary(data) do
CryptoS3.mac_chunk(state.encrypt_mac_state, data)
end
defp mac_chunk({:error, reason}, _state, _data) do
{:error, reason}
end
end