Okay, so I’m not sure exactly how I’ll format this in the future, but I figure it’s useful to send some “companion code” around with the bullshit assertions I make to the world, about tech stuff, while under-the-influence.
So, yesterday was very system-y, today we’re gonna get down to the “in practice” portion of the actual encryption/decryption that will facilitate this miraculously robust and infinitely scalable e2e encrypted, content sharing system, which only lives in my head.
Anyway…
Here’s the implementation for the onion’d symmetrical, stream-cipher, ChaCha20
blended nicely with the asymmetrical, elliptic-curve-cryptography algo, x25519
which facilitates key exchange and lends itself to the public/private key stuff necessary for the e2e encrypted-ness of the system. And finally, signed using
another ECC algo - this one oriented toward cryptographic signing - ed25519
.
Encrypting and Signing
Before we dive into the nitty-gritty, it’s important for us to first specify the I/O for our cryptographic superset(?). In order to encrypt content
for our set of users, we will need all the receiver_pub_exchange_keys
(these are stored in the pod’s encrypted metadata). We will also need the raw content
which will be encrypted, and finally, we need the ed25519
signing_key
. Once we’ve encrypted the content, we need to grab out the values which will be stored
and transmitted. These items are the “top level” nonce
(this is the nonce which is paired with the encrypted_content
at decryption time), the “list” (it’s actually)
a single, continuous “byte array” or in Rust terms, a Vec<u8>
(more on formatting in a sec), the actually encrypted content, and the encrypted content’s signature.
At the end, our function signature should look something like this:
pub fn encrypt_and_sign_content(
// the sender's signing key, later used for verification
// on the server, at write time, *and* by other users
// verifying the source of the content
signing_key: [u8; 32],
// raw content to be encrypted
content: Vec<u8>,
// list of public exchange keys for all
// recipients. this list is retrieved from
// the target pod's "pub exchange key" registry
receiver_pub_exchange_keys: Vec<[u8; 32]>,
) -> ([u8; 12], Vec<u8>, Vec<u8>) { // (nonce, key_sets, encrypted_content)
// TODO: do stuff...
}
A word on formatting: there are multiple situations where we are choosing to interact with bytes and join/prefix byte arrays
with things like signatures, or instead of serializing for each individual record’s properties in a struct
, and it allows
us to avoid additional storage overhead.
Now to dig into the “key sets”, which are potentially the spot where most people could get tripped up. The key_sets
property, is a contiguous byte array, with each recipient’s
key_set
actually being its own contiguous, fixed length, array of bytes. The format of a given recipient’s key_set
looks as follows:
// 256 [u8; 32] receiver_pub_exchange_key -> for indexing into the indexedDB table that has the AES + passcode encrypted private key
// 256 [u8; 32] sender_pub_exchange_key -> for pairing with the AES + passcode encrypted key to create a shared secret to decrypt the once content key
// 96 [u8; 12] encrypted_content_key_nonce -> for use with the encrypted content key
// 256 [u8; 32] encrypted_content_key -> decrypted by the shared secret and nonce
[...receiver_pub_exchange_key, ...sender_pub_exchange_key, ...encrypted_content_key_nonce, ...encrypted_content_key]
Given that each “list item” or key_set
is of a fixed length, we can easily split and iterate through. And one nest deeper, we can do the same with the keys themselves, which
are also of fixed length.
the
key_sets
value is a contiguous byte array of fixed-lengthkey_set
s, which themselves are composed of fixed-length keys
And then finally, the encrypted_content
is just that - but with a little extra. The encrypted_content
is actually the encrypted content, prefixed with the [u8; 64]
“External” signature,
which will be used by the server to validate the authenticity of the request.
“External” signing is when the encrypted content is prefixed with a signature. “Internal” signing is when the content itself is prefixed with a signature, prior to encryption
“External” signing is something that will be done in this algorithm, as it is needed for the server to verify the authenticity of the sender. However this signature is used only once, and “ripped” from the payload before writing to the database, as to better preserve the anonymity of the sender where it is stored. Recipients will be able to verify the authenticity of content via the “Internal” signature, which is itself included in the encrypted payload.
The Guts
Just so you have context, we’re gonna use a few external packages and my Cargo.toml
looks like the this:
[package]
name = "crypto"
version = "0.1.0"
edition = "2021"
[dependencies]
rand_core = "0.6.4"
aes = "0.8.3"
chacha20poly1305 = "0.10.1"
x25519-dalek = { version = "2.0.0-rc.3", features = ["static_secrets"] }
ed25519-dalek = { version = "2.0.0-rc.3", features = ["rand_core"] }
For this first chunk (the complex/blended/hybrid/whatever encryption algorithm), we’ll only actually depend on chacha20poly1305
, x25519-dalek
and ed25519-dalek
.
For the sake of keeping things consistent, I’ll use the aes
crate to show how I’ll encrypt content locally, but it’s really simple and following
the aes
crate docs is really straightforward.
As described yesterday, our super fun function, first, creates a “once key” or the “one time use, private key for symmetrical encryption of the actual content”,
which enables us to build the cipher which we’ll use to encrypt our content (once, for all users). With ChaCha20
we also need to generate a nonce
which is
a random number that is technically “safe” to share publicly and will be bolted onto the stored record later, after being used to encrypt the content.
This is pretty simple and looks like this:
// ChaCha20 private key creation for *one time use* with this message *only*
let private_content_key = chacha20poly1305::ChaCha20Poly1305::generate_key(&mut rand_core::OsRng);
// the cipher which can actually be used to encrypt the content
let content_cipher = chacha20poly1305::ChaCha20Poly1305::new(&private_content_key);
// a once generated random number that ChaCha20 needs for encryption and decryption time
let nonce = chacha20poly1305::ChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
Okay, so now that we have our “once private key” (and all its associated friends) created, we can start to do some interesting things to the content.
We now have all the information we need to encrypt the raw content (remember, we only encrypt content once, then we “hide” the keys used to encrypt, with
with the shared secrets generated via the x25519
pub/priv key pairing). But we also need to do our “internal” signing so that users who decrypt our
payload, can verify its authenticity. So… the process for signing, and encrypting looks like this:
// `&signing_key` is a reference to the `signing_key: [u8; 32]` passed into the `encrypt_and_sign_content` fn
let signing_key = ed25519_dalek::SigningKey::from_bytes(&signing_key);
// `&content` same as above, in that it comes from the `content: Vec<u8>` param off the `encrypt_and_sign_content` fn
let content_signature_internal = signing_key.sign(&content).to_vec();
// the next 2 lines merge the `signature` and `content` - essentially [...signature, ...content]
let mut content_prefixed_with_signature: Vec<u8> = content_signature_internal.clone();
content_prefixed_with_signature.extend(content);
// encrypt the content with the `nonce`, `content` which has been prefixed with its `signature`
let encrypted_content = content_cipher.encrypt(
&nonce,
content_prefixed_with_signature.as_ref()
).expect("failed to encrypt content.");
So at this point we have our content encryption done. We’re left with the encrypted content, and the private key and nonce which are needed
to decrypt the encrypted content. Now we have to figure out how to store this one-time ChaCha20
private key in a way that makes it only accessible
to the intended recipients of this message.
Well, we know that we need to use some kind of asymmetrical, private/public key encryption scheme which will allow us to “hide” this key on the record.
Asymmetrical, private/public key, encryption? Omg, looks like this is where the x25519
comes in. So… to cut to the chase… we’re gonna encrypt the
one-time ChaCha20
private key with the “shared secret” that can only be generated from the pairing of another (but this time x25519
) one-time private
key, and the publicly registered, public key, of an intended recipient. And by storing the public key associated to this “one time” sender_private_key
,
despite its private counterpart - more or less - being “thrown away”, that sender_public_exchange_key
can be pared with the private key which corresponds
to the receiver_public_exchange_key
that was used to generate the initial shared secret.
long story short: trust me that the
x25519
public/private key exchange hell, does make sense and is secure, even if I’m struggling to explain how
The code for handling this one-time pub/priv x25519
which is used to generate the shared secret with the publicly registered recipient_public_exchange_key
looks like this:
// the "one-time" keys generated for this particular message
let sender_priv_exchange_key = x25519_dalek::EphemeralSecret::random_from_rng(rand_core::OsRng);
let sender_pub_exchange_key = x25519_dalek::PublicKey::from(&sender_priv_exchange_key);
// the "shared secret" created by the pairing of `receiver_pub_exchange_key` and the brand new, one-time `sender_priv_exchange_key`
let shared_secret = sender_priv_exchange_key.diffie_hellman(&x25519_dalek::PublicKey::from(receiver_pub_exchange_key));
// creates the ChaCha20 cipher that is keyed with the `shared_secret` generated above
let shared_secret_cipher = chacha20poly1305::ChaCha20Poly1305::new(shared_secret.as_bytes().into());
// the `nonce` used to decrypt the `encrypted_content_key`
let encrypted_content_key_nonce = chacha20poly1305::ChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
// encrypts the `private_content_key` generated for *content* encryption earlier
let encrypted_content_key = shared_secret_cipher.encrypt(
&encrypted_content_key_nonce,
private_content_key.as_ref()
).expect("failed to encrypt content");
Obviously we’re doing a couple things in order here (and keep in mind this is just for once recipient and we have a list of recipients):
- Creating some
x25519
exchange keys which will only be valid for this message - Generating a shared secret with the one-time private key
- Encrypting the
ChaCha20
content key with the generated shared secret
So, what?
Well, now we have some more pieces (again just for this one recipient) which will allow us to construct their key_set
and bolt that key_set
onto the end of the key_sets
byte array. To create the key_set
based on the constraints we outlined above, and to stay consistent with formatting
we do the following:
// initialize vector that will hold the bytes
let mut key_set: Vec<u8> = vec![];
// cram that shit in their, just make sure you do it in order
key_set.extend(receiver_pub_exchange_key.iter());
key_set.extend(sender_pub_exchange_key.as_bytes());
key_set.extend(encrypted_content_key_nonce);
key_set.extend(encrypted_content_key);
So, cool. Now we have a keyset for a recipient, and with these 4 items and the rest of the payload, we have the ability to decrypt with the information we have securely stored, locally.
As I mentioned, we need to create a list of key_sets
which are just that once key_set
smashed onto the end of another
byte array called key_sets
. Just to put the whole thing together, the for
loop’d version looks something like:
// !! stored as a contiguous "byte array" or `Vec<u8>` which represents list of all the recipient-specific "key sets."
// !! each "key set" is set in the following format, and "butts up" against the next segment for the next recipient
// !! which is also of equal length (256 + 256 + 96 + 256).
// 256 receiver_pub_exchange_key -> for indexing into the indexedDB table that has the AES + passcode encrypted private key
// 256 sender_pub_exchange_key -> for pairing with the AES + passcode encrypted key to create a shared secret to decrypt the once content key
// 96 encrypted_content_key_nonce -> for use with the encrypted content key
// 256 encrypted_content_key -> decrypted by the shared secret and nonce
let mut key_sets: Vec<u8> = vec![];
for receiver_pub_exchange_key in receiver_pub_exchange_keys {
let sender_priv_exchange_key = x25519_dalek::EphemeralSecret::random_from_rng(rand_core::OsRng);
let sender_pub_exchange_key = x25519_dalek::PublicKey::from(&sender_priv_exchange_key);
let shared_secret = sender_priv_exchange_key.diffie_hellman(&x25519_dalek::PublicKey::from(receiver_pub_exchange_key));
let shared_secret_cipher = chacha20poly1305::ChaCha20Poly1305::new(shared_secret.as_bytes().into());
let encrypted_content_key_nonce = chacha20poly1305::ChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
let encrypted_content_key = shared_secret_cipher.encrypt(
&encrypted_content_key_nonce,
private_content_key.as_ref()
).expect("failed to encrypt content");
key_sets.extend(receiver_pub_exchange_key.iter());
key_sets.extend(sender_pub_exchange_key.as_bytes());
key_sets.extend(encrypted_content_key_nonce);
key_sets.extend(encrypted_content_key);
}
putting it together for all recipients, in a loop
Sorry for the duplicate comments in the code block, I just know some people only like to look at code, and think reading is for nerds.
Finally, we need to add the “external” signature (for server verification), and below our key_sets
building, for
loop,
we do the following to deliver that outcome:
let encrypted_content_signature_external = signing_key.sign(&encrypted_content).to_vec();
let mut encrypted_content_prefixed_with_signature = encrypted_content_signature_external.clone();
encrypted_content_prefixed_with_signature.extend(encrypted_content);
prefix the “external” signature to the encrypted content
And because chunks of code, broken up over an article, without including the whole contiguous chunk somewhere is the
most annoying thing in the world, here’s the whole encrypt_and_sign_content
thingy:
use chacha20poly1305::aead::{Aead, AeadCore, KeyInit as ChaChaKeyInit};
use ed25519_dalek::Signer;
pub fn encrypt_and_sign_content(
signing_key: [u8; 32],
content: Vec<u8>,
receiver_pub_exchange_keys: Vec<[u8; 32]>,
) -> ([u8; 12], Vec<u8>, Vec<u8>) {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&signing_key);
let private_content_key =
chacha20poly1305::ChaCha20Poly1305::generate_key(&mut rand_core::OsRng);
let content_cipher = chacha20poly1305::ChaCha20Poly1305::new(&private_content_key);
let nonce = chacha20poly1305::ChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
let content_signature_internal = signing_key.sign(&content).to_vec();
let mut content_prefixed_with_signature: Vec<u8> = content_signature_internal.clone();
content_prefixed_with_signature.extend(content);
let encrypted_content = content_cipher
.encrypt(&nonce, content_prefixed_with_signature.as_ref())
.expect("failed to encrypt content");
let mut key_sets: Vec<u8> = vec![];
for receiver_pub_exchange_key in receiver_pub_exchange_keys {
let sender_priv_exchange_key =
x25519_dalek::EphemeralSecret::random_from_rng(rand_core::OsRng);
let sender_pub_exchange_key = x25519_dalek::PublicKey::from(&sender_priv_exchange_key);
let shared_secret = sender_priv_exchange_key
.diffie_hellman(&x25519_dalek::PublicKey::from(receiver_pub_exchange_key));
let shared_secret_cipher =
chacha20poly1305::ChaCha20Poly1305::new(shared_secret.as_bytes().into());
let encrypted_content_key_nonce =
chacha20poly1305::ChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
let encrypted_content_key = shared_secret_cipher
.encrypt(&encrypted_content_key_nonce, private_content_key.as_ref())
.expect("failed to encrypt content");
key_sets.extend(receiver_pub_exchange_key.iter());
key_sets.extend(sender_pub_exchange_key.as_bytes());
key_sets.extend(encrypted_content_key_nonce);
key_sets.extend(encrypted_content_key);
}
let encrypted_content_signature_external = signing_key.sign(&encrypted_content).to_vec();
let mut encrypted_content_prefixed_with_signature: Vec<u8> =
encrypted_content_signature_external.clone();
encrypted_content_prefixed_with_signature.extend(encrypted_content);
(
nonce.into(),
key_sets,
encrypted_content_prefixed_with_signature,
)
}
So yea, that’s some fun encrypting. Time to decrypt and verify.
Decrypting and Verifying
Whelp, now we know how to encrypt our content, and unless there’s a some gaping hole in my strategy, or understanding of cryptography, but that ball should be pretty resilient.
So, when we start with decryption and verification, we also have to start at I/O. As a “decrypting entity” we, fortunately, get
to care about fewer parts of “the ball” than the encrypting entity does. For one, we only care about our key_set
, so right away
we can start listing out our parameters with that in mind.
In order to verify the signature (prefixed to and encrypted in the same “ball” as the content), we need the sender’s public verification
key. In order to get this, we can pull it off the user’s account (or some local cache, or pod metadata). For this reason, the sender_pub_verification_key
is the first parameter we accept in our decrypt_and_verify
fn.
The next two pieces of data we need are the sender_pub_exchange_key
(this is that one-time public x25519
key created, new, for each message), and the
receiver_priv_exchange_key
(this key is stored in a local database and is AES256 encrypted, but addressable by the public key it was created alongside).
As we remember, the receiver_pub_exchange_key
is stored in the key_set
and allows us to retrieve the encrypted private key that is needed to generate
the shared secret for that particular message.
Next, we need the encrypted_content_key_nonce
(for decryption of the one-time generated, private ChaCha20
key), and the encrypted_content_key
itself (which, again,
is encrypted using the shared secret, generated by the pairing of the sender_public_exchange_key
and the receiver_private_exchange_key
).
And for the last parameters, we need the actual encrypted_content
and the content_nonce
(which is paired with the x25519
, computed, shared secret to decrypt the encrypted_content
).
For the output, we just need the Vec<u8>
which is the “decrypted content.”
So, after all those nonsense words, we should start with a function signature that looks something like this:
pub fn decrypt_and_verify(
// used to verify the payload *inside* the encrypted "ball"
sender_pub_verification_key: [u8; 32],
// used to create the shared secret, which is the
// key for the `encrypted_content_key` and
// pairs with the `encrypted_content_key_nonce`
sender_pub_exchange_key: [u8; 32],
receiver_priv_exchange_key: [u8; 32],
// for use "unpacking" or decrypting the immediately
// following `encrypted_content_key`. is paired
// with the shared secret, derived from the
// `sender_pub_exchange_key` and `receiver_priv_exchange_key`
encrypted_content_key_nonce: [u8; 12],
encrypted_content_key: [u8; 32],
// used when decrypting the `encrypted_content` param below
content_nonce: [u8; 12],
encrypted_content: Vec<u8>,
) -> Vec<u8> {
// TODO: fill in the guts...
}
Because we don’t have any special formatting for any of the values that are passed in (stupid key_sets
… why make things more complicated?),
we can jump right back into #TheGuts.
The Guts
The first thing to recognize is that all of these values are being passed around as either fixed length byte arrays, or Vec<u8>
s. The
reason for this is that all other systems should be able to reasonably think about just storing keys as bytes, and then inside our fun,
little land of cryptography, we deal with all the crate-specific APIs.
That being said, the first thing we have to do is convert them all into structs we can actually work with, so you’ll see some of that conversion happen in various places throughout the example.
Okay, now that we have the function signature and our I/O defined, we can first convert our fixed-length, byte arrays into public and secret x25519
keys,
and generate a ChaCha20
cipher, based on the shared secret, derived from the sender_pub_exchange_key
and receiver_priv_exchange_key
.
// "inflate" the fixed-length, byte array into a useful struct
let sender_pub_exchange_key = x25519_dalek::PublicKey::from(sender_pub_exchange_key);
let receiver_priv_exchange_key = x25519_dalek::StaticSecret::from(receiver_priv_exchange_key);
// derive the shared secret from the `x25519` keys
let shared_secret = receiver_priv_exchange_key.diffie_hellman(&sender_pub_exchange_key);
// create the cipher which is keyed with the `shared_secret`
let shared_secret_cipher = chacha20poly1305::ChaCha20Poly1305::new(shared_secret.as_bytes().into());
create the
ChaCha20
cipher for decrypting the content key
Now that our cipher has been created based on the shared secret, we need to decrypt the “content key”
(ya know, that ChaCha20
private key we used to encrypt the content at the very beginning). That will look like this:
// decrypt the "content key" which can be used to decrypt the highest level `encrypted_content`
let decrypted_content_key = match shared_secret_cipher.decrypt(
&chacha20poly1305::Nonce::from(encrypted_content_key_nonce),
encrypted_content_key.as_ref(),
).expect("failed to decrypt content key");
Once we’ve decrypted the content key, we need to now create the ChaCha20
cipher which will unlock THE CONTENT (hurray! finally to the shit that
we actually need in order to display the useful content). And once we’ve created the cipher, we can extract the content! That looks like this:
// creates cipher, keyed by the decrypted *content* key
let content_cipher = chacha20poly1305::ChaCha20Poly1305::new(GenericArray::from_slice(&decrypted_content_key));
// convert the `content_nonce` to a `Nonce` and decrypt the `encrypted_content`
let mut content = content_cipher
.decrypt(&chacha20poly1305::Nonce::from(content_nonce), encrypted_content.as_ref())
.expect("failed to decrypt content");
Now that our content is decrypted, we’re actually not quite ready - we still need to verify. Our content
(post decryption)
is - if you remember - prefixed with the signature for the content. So in order to verify this signature, we first need to extract
the leading bytes, and then we need to use ed25519
to actually verify the content against the signature…
// uses the `sender_pub_verification_key` from the arguments passed into the fn
let verifying_key = match ed25519_dalek::VerifyingKey::from_bytes(&sender_pub_verification_key).expect("failed to create verifying key");
// creates a fixed array where the signature bytes can be dropped in right quick
let mut signature_fixed_bytes: [u8; 64] = [0; 64];
// gets the signature off the first strip of bytes and drains the bytes from the `content` Vec
if content.len() >= 64 {
signature_fixed_bytes.copy_from_slice(&content[..64]);
content.drain(..64);
}
// initializes the signature struct with the signature bytes
let signature = ed25519_dalek::Signature::from_bytes(&signature_fixed_bytes);
// verify the signature against the content
let verified = verifying_key.verify(&content, &signature).is_ok();
And now we have both decrypted AND verified the content of the message
To show the whole thing together:
use chacha20poly1305::aead::{generic_array::GenericArray, Aead, KeyInit as ChaChaKeyInit};
use ed25519_dalek::Verifier;
pub fn decrypt_and_verify(
sender_pub_verification_key: [u8; 32],
sender_pub_exchange_key: [u8; 32],
receiver_priv_exchange_key: [u8; 32],
encrypted_content_key_nonce: [u8; 12],
encrypted_content_key: [u8; 32],
content_nonce: [u8; 12],
encrypted_content: Vec<u8>,
) -> Vec<u8> {
let sender_pub_exchange_key = x25519_dalek::PublicKey::from(sender_pub_exchange_key);
let receiver_priv_exchange_key = x25519_dalek::StaticSecret::from(receiver_priv_exchange_key);
let shared_secret = receiver_priv_exchange_key.diffie_hellman(&sender_pub_exchange_key);
let shared_secret_cipher =
chacha20poly1305::ChaCha20Poly1305::new(shared_secret.as_bytes().into());
let decrypted_content_key = shared_secret_cipher
.decrypt(
&chacha20poly1305::Nonce::from(encrypted_content_key_nonce),
encrypted_content_key.as_ref(),
)
.expect("failed to decrypt content key");
let content_cipher =
chacha20poly1305::ChaCha20Poly1305::new(GenericArray::from_slice(&decrypted_content_key));
let mut content = content_cipher
.decrypt(
&chacha20poly1305::Nonce::from(content_nonce),
encrypted_content.as_ref(),
)
.expect("failed to decrypt content");
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&sender_pub_verification_key)
.expect("failed to create verifying key");
let mut signature_fixed_bytes: [u8; 64] = [0; 64];
if content.len() >= 64 {
signature_fixed_bytes.copy_from_slice(&content[..64]);
content.drain(..64);
}
let signature = ed25519_dalek::Signature::from_bytes(&signature_fixed_bytes);
let verified = verifying_key.verify(&content, &signature).is_ok();
if verified {
content
} else {
// handle your own damn error cases
vec![]
}
}
Okay, so encryption and decryption of the content which will traverse the “system” - or go through our backend and be stored in our database, is wrapped (assuming that no angry cryptographic experts tell me that my shit is junk).
Less impressive but maybe yet another opportunity to try and poorly explain the “local storage” model, we can now talk about the
AES256
usage for on-device, key encryption.
Local Encryption
So, clearly (and today I guess I’ve switched to “So,” as my annoying paragraph entrypoint) we’ve got a lot of keys moving around,
all the time, so that we can keep this data secure. Well, in order to maintain all of this security, we have to do a great job of managing our private - and to a lesser degree, public - keys safely. Because of the “batched forward secrecy” we do need a way to
store the history of “one off” private keys that were used for a given “batch” of secrecy. They need to be addressable, but they also
need to remain encrypted at all times when not in use. These “batch keys” or “private batch keys” are all stored in an encrypted format,
using AES256
encryption. This is the encryption standard that is used to store credit card numbers for payment processors and such.
So, the way we can preserve their encryption, while also preserving their ability to be addressable, we store the k/v pair as the “plaintext” (it’s actually a Uint8Array
) public x25519
key as the “k” in the “k/v”, and the AES256
encrypted x25519
private key (used for a “batch”) as the “v” in the “k/v.”
The reason why this works is that the public x25519
key that was used to generate the shared secret with the one-time sender_private_exchange_key
, is stored in the key_set
for the recipient of the message. This means, that without revealing
who the recipient is on the stored object, we give the recipient both the ability to get their key_set
off a given message, using
the “batched” public key, which is only ever stored (though it’s in plaintext) on the recipient’s device AND the recipient can use
that same public key to track down their “batched” receiver_private_exchange_key
(in its AES256
encrypted form).
This allows the user to essentially have a floating, ever evolving, disjointed, yet robust, routing table which is mostly opaque to anyone who is able to observe even the most details of the running system.
So anyway, the AES256
encryption algo that we use to encrypt the “private batch keys” is really boring, and as follows:
The Code
use aes::{
cipher::{generic_array::GenericArray, BlockEncrypt},
Aes256,
};
pub fn block_encrypt(key: [u8; 32], content: Vec<u8>) -> Vec<u8> {
let cipher = Aes256::new(GenericArray::from_slice(&key));
let mut gen_arr_encrypted_content = GenericArray::clone_from_slice(&content);
cipher.encrypt_block(&mut gen_arr_encrypted_content);
gen_arr_encrypted_content.to_vec()
}
pub fn block_decrypt(key: [u8; 32], encrypted_content: Vec<u8>) -> Vec<u8> {
let cipher = Aes256::new(GenericArray::from_slice(&key));
let mut gen_arr_content = GenericArray::clone_from_slice(&encrypted_content);
cipher.decrypt_block(&mut gen_arr_content);
gen_arr_content.to_vec()
}
So anyway, that’s kinda what that looks like.
Conclusion
Again, not really a conclusion for this one, other than that I’ve left a fun “black hole” that is what the server and database implementations look like. I probably won’t share many of those details, but genuinely do want/need additional eyes on the overall encryption mechanisms, so this is “just throwin’ it out there.”