use crate::{
keys::{
ed25519::PublicKey,
x25519::{diffie_hellman, X25519PublicKey, X25519SecretKey},
KeyPair,
},
rand::{self, UnspecifiedRandError},
};
use arrayvec::{ArrayString, ArrayVec};
use std::convert::TryInto;
use thiserror::Error;
const MAGIC_VALUE: [u8; 2] = [0x27, 0xb6];
const MAX_ENCRYPTED_LEN: usize = MAX_MSG_LEN + 1;
const MAX_B64_LEN: usize = 255;
const MAX_LEN: usize = MAX_MSG_LEN + META_LEN + 1;
const MAX_MSG_LEN: usize = 170;
const META_LEN: usize = 4 + NONCE_LEN;
const NONCE_LEN: usize = 16;
const V1: u8 = 1;
type Nonce = [u8; NONCE_LEN];
#[derive(Clone, Debug, Error)]
pub enum DecryptTxCommentErr {
#[error("Invalid base 64 encoding: {0}")]
Base64Err(base64::DecodeError),
#[error("Too short data")]
TooShort,
#[error("Too long data")]
TooLong,
#[error("malicious input")]
MaliciousInput,
#[error("Invalid utf8 message: {0}")]
Utf8Err(std::str::Utf8Error),
#[error("Unsupported version")]
UnsupportedVersion,
#[error("Wrong format")]
WrongFormat,
#[error("Wrong magic value")]
WrongMagicValue,
}
#[allow(missing_copy_implementations)]
pub struct SharedKey([u8; 32]);
pub fn compute_shared_key<K: KeyPair>(my_keypair: &K, its_pubkey: &PublicKey) -> SharedKey {
let precomputed_key = diffie_hellman(
X25519SecretKey::from_bytes(my_keypair.scalar_bytes_without_normalization()),
X25519PublicKey::from(its_pubkey),
|shared_secret| {
let mut buffer = [0; 32];
cryptoxide::salsa20::hsalsa20(shared_secret, &[0; 16], &mut buffer);
buffer
},
);
let mut shared_key = SharedKey([0; 32]);
shared_key.0.copy_from_slice(&precomputed_key[..]);
shared_key
}
pub fn get_message_type(
encrypted_tx_comment: &ArrayString<MAX_B64_LEN>,
) -> Result<u8, DecryptTxCommentErr> {
let (version, bytes) = verify_magic_value_and_read_version(encrypted_tx_comment)?;
if version != V1 {
return Err(DecryptTxCommentErr::UnsupportedVersion);
}
if bytes.len() <= META_LEN {
return Err(DecryptTxCommentErr::TooShort);
}
let message_type = bytes[3];
Ok(message_type)
}
pub fn decrypt_tx_comment<K: KeyPair>(
my_keypair: &K,
other_pubkey: &PublicKey,
encrypted_tx_comment: &ArrayString<MAX_B64_LEN>,
) -> Result<ArrayString<MAX_MSG_LEN>, DecryptTxCommentErr> {
let shared_key = compute_shared_key::<K>(my_keypair, other_pubkey);
decrypt_tx_comment_with_shared_key(&shared_key, encrypted_tx_comment)
}
pub fn decrypt_tx_comment_with_shared_key(
shared_key: &SharedKey,
encrypted_tx_comment: &ArrayString<MAX_B64_LEN>,
) -> Result<ArrayString<MAX_MSG_LEN>, DecryptTxCommentErr> {
let (version, bytes) = verify_magic_value_and_read_version(encrypted_tx_comment)?;
if version != V1 {
return Err(DecryptTxCommentErr::UnsupportedVersion);
}
if bytes.len() <= META_LEN {
return Err(DecryptTxCommentErr::TooShort);
}
let mut nonce = [0; NONCE_LEN];
nonce.copy_from_slice(&bytes[4..20]);
let encrypted_data = &bytes[META_LEN..];
let mut encryption_key = ArrayVec::<u8, MAX_ENCRYPTED_LEN>::new();
unsafe {
encryption_key.set_len(encrypted_data.len());
}
crate::scrypt::scrypt(
&shared_key.0[..],
&nonce[..],
&crate::scrypt::params::ScryptParams {
log_n: 10,
r: 8,
p: 1,
},
&mut encryption_key,
);
let mut decrypted_data = ArrayVec::<u8, MAX_ENCRYPTED_LEN>::new();
for i in 0..encrypted_data.len() {
decrypted_data.push(encryption_key[i] ^ encrypted_data[i]);
}
read_decrypted_data(decrypted_data.as_ref())
}
pub fn encrypt_tx_comment_v1<K: KeyPair>(
message_type: u8,
my_keypair: &K,
other_pubkey: &PublicKey,
tx_comment: &ArrayString<MAX_MSG_LEN>,
) -> Result<ArrayString<MAX_B64_LEN>, UnspecifiedRandError> {
let message_len = tx_comment.len();
let message_len_plus_one = message_len + 1;
let mut data_to_encrypt = ArrayVec::<u8, MAX_ENCRYPTED_LEN>::new();
data_to_encrypt.push(message_len as u8);
data_to_encrypt
.try_extend_from_slice(tx_comment.as_bytes())
.unwrap_or_else(|_| unsafe { std::hint::unreachable_unchecked() });
let extra_bytes_len = rand::gen_u8()? % (MAX_MSG_LEN - message_len) as u8;
let data_to_encrypt_len = message_len_plus_one + extra_bytes_len as usize;
unsafe {
data_to_encrypt.set_len(data_to_encrypt_len);
}
rand::gen_random_bytes(&mut data_to_encrypt[message_len_plus_one..])?;
let mut bytes = ArrayVec::<u8, MAX_LEN>::new();
bytes.push(MAGIC_VALUE[0]); bytes.push(MAGIC_VALUE[1]); bytes.push(V1);
bytes.push(message_type);
let nonce = gen_nonce()?;
unsafe {
bytes.set_len(META_LEN);
}
bytes[4..].copy_from_slice(&nonce[..]);
let shared_key = compute_shared_key::<K>(my_keypair, other_pubkey);
let mut encryption_key = ArrayVec::<u8, MAX_ENCRYPTED_LEN>::new();
unsafe {
encryption_key.set_len(data_to_encrypt_len);
}
crate::scrypt::scrypt(
&shared_key.0[..],
&nonce[..],
&crate::scrypt::params::ScryptParams {
log_n: 10,
r: 8,
p: 1,
},
&mut encryption_key,
);
for i in 0..data_to_encrypt_len {
bytes.push(encryption_key[i] ^ data_to_encrypt[i]);
}
let mut b64_str = ArrayString::new();
unsafe {
b64_str.set_len(MAX_B64_LEN);
}
let bytes_written = base64::encode_config_slice(bytes, base64::STANDARD_NO_PAD, unsafe {
b64_str.as_bytes_mut()
});
unsafe {
b64_str.set_len(bytes_written);
}
Ok(b64_str)
}
#[cfg(not(test))]
fn gen_nonce() -> Result<Nonce, UnspecifiedRandError> {
let mut nonce = [0u8; NONCE_LEN];
crate::rand::gen_random_bytes(&mut nonce[..])?;
Ok(nonce)
}
#[cfg(test)]
#[allow(clippy::unnecessary_wraps)]
fn gen_nonce() -> Result<Nonce, UnspecifiedRandError> {
Ok([
0x16, 0x74, 0x02, 0x85, 0x72, 0x1a, 0xde, 0xb1, 0x0d, 0xa0, 0x41, 0x06, 0xc9, 0xeb, 0x9e,
0x34,
])
}
fn read_decrypted_data(
decrypted_data: &[u8],
) -> Result<ArrayString<MAX_MSG_LEN>, DecryptTxCommentErr> {
let real_message_length = decrypted_data[0] as usize;
if real_message_length >= decrypted_data.len() {
return Err(DecryptTxCommentErr::MaliciousInput);
}
std::str::from_utf8(&decrypted_data[1..=real_message_length])
.map_err(DecryptTxCommentErr::Utf8Err)?
.try_into()
.map_err(|_| DecryptTxCommentErr::TooLong)
}
fn verify_magic_value_and_read_version(
encrypted_tx_comment: &ArrayString<MAX_B64_LEN>,
) -> Result<(u8, ArrayVec<u8, MAX_LEN>), DecryptTxCommentErr> {
let mut bytes = ArrayVec::new();
unsafe {
bytes.set_len(MAX_LEN);
}
let bytes_written = base64::decode_config_slice(
encrypted_tx_comment.as_bytes(),
base64::STANDARD_NO_PAD,
&mut bytes,
)
.map_err(DecryptTxCommentErr::Base64Err)?;
unsafe {
bytes.set_len(bytes_written);
}
if bytes.len() < 3 {
return Err(DecryptTxCommentErr::TooShort);
}
if bytes[0] != MAGIC_VALUE[0] || bytes[1] != MAGIC_VALUE[1] {
return Err(DecryptTxCommentErr::WrongMagicValue);
}
Ok((bytes[2], bytes))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
bases::b58::ToBase58,
keys::{
ed25519::{KeyPairFromSeed32Generator, PublicKey},
PublicKey as _,
},
seeds::Seed32,
};
use unwrap::unwrap;
#[test]
fn test_read_malicious_decrypted_data() {
let decrypted_data = &[42, 1];
assert!(read_decrypted_data(decrypted_data).is_err());
}
#[test]
fn test_decrypt_tx_comment() -> Result<(), DecryptTxCommentErr> {
let seed_bob = Seed32::new([
33, 73, 85, 181, 88, 199, 121, 50, 104, 88, 158, 85, 126, 218, 42, 182, 155, 82, 147,
183, 61, 57, 7, 248, 44, 130, 225, 45, 105, 196, 114, 33,
]);
println!("seed_bob={}", hex::encode(&seed_bob));
let kp_bob = KeyPairFromSeed32Generator::generate(seed_bob);
assert_eq!(
&kp_bob.public_key().to_base58(),
"8txjWNFZhMJbKPijvnFybeksN1QpKaKJrM4jW8HhnFsX"
);
let pk_alice = unwrap!(PublicKey::from_base58(
"EVfy1VoZwbuN7L69kYiHxeosJLh5azkHV8G6TaSLy94r"
));
let shared_key = compute_shared_key(&kp_bob, &pk_alice);
let encrypted_tx_comment = unwrap!(ArrayString::from("J7YBABZ0AoVyGt6xDaBBBsnrnjREXG3NgZKjIyN2lxEx+jbNmf/tZt6uMaelMGRx48kqEovl70uUU17l1J8FOy9i"));
let tx_comment = decrypt_tx_comment_with_shared_key(&shared_key, &encrypted_tx_comment)?;
assert_eq!(
tx_comment.as_str(),
"My taylor is rich ? Isn't it ? Un été 42..."
);
Ok(())
}
#[test]
fn test_encrypt_tx_comment() {
let seed_alice = Seed32::new([
12, 106, 21, 208, 0, 77, 36, 164, 15, 101, 3, 48, 13, 73, 113, 3, 47, 176, 87, 255,
125, 194, 41, 214, 81, 104, 59, 65, 60, 150, 162, 22,
]);
println!("seed_alice={}", hex::encode(&seed_alice));
let kp_alice = KeyPairFromSeed32Generator::generate(seed_alice);
assert_eq!(
&kp_alice.public_key().to_base58(),
"EVfy1VoZwbuN7L69kYiHxeosJLh5azkHV8G6TaSLy94r"
);
let pk_bob = unwrap!(PublicKey::from_base58(
"8txjWNFZhMJbKPijvnFybeksN1QpKaKJrM4jW8HhnFsX"
));
let tx_comment = unwrap!(ArrayString::from(
"My taylor is rich ? Isn't it ? Un été 42..."
));
println!("tx_comment_len={}", tx_comment.len());
let encrypted_comment = unwrap!(encrypt_tx_comment_v1(0, &kp_alice, &pk_bob, &tx_comment));
println!("encrypted_comment={}", encrypted_comment);
assert_eq!(
&encrypted_comment[..88], "J7YBABZ0AoVyGt6xDaBBBsnrnjREXG3NgZKjIyN2lxEx+jbNmf/tZt6uMaelMGRx48kqEovl70uUU17l1J8FOy9i"
);
}
#[test]
fn test_cryptobox_shared_key() {
use crate::keys::ed25519::Ed25519KeyPair;
use crate::keys::inner::KeyPairInner;
use sodiumoxide::crypto::box_;
use sodiumoxide::crypto::box_::curve25519xsalsa20poly1305::PublicKey as SodiumPublicKey;
use sodiumoxide::crypto::box_::curve25519xsalsa20poly1305::SecretKey as SodiumSecretKey;
let kp1 = unwrap!(Ed25519KeyPair::generate_random());
let kp2 = unwrap!(Ed25519KeyPair::generate_random());
let sk1 = X25519SecretKey::from_bytes(kp1.scalar_bytes_without_normalization());
let sk2 = X25519SecretKey::from_bytes(kp2.scalar_bytes_without_normalization());
let pk1 = X25519PublicKey::from(&kp1.public_key());
let pk2 = X25519PublicKey::from(&kp2.public_key());
let our_precomputed_key =
box_::precompute(&SodiumPublicKey((pk2.0).0), &SodiumSecretKey(sk1.0));
let their_precomputed_key =
box_::precompute(&SodiumPublicKey((pk1.0).0), &SodiumSecretKey(sk2.0));
assert_eq!(our_precomputed_key.0, their_precomputed_key.0);
let shared_key = compute_shared_key(&kp1, &kp2.public_key());
assert_eq!(&shared_key.0[..], &their_precomputed_key.0[..]);
let shared_key = compute_shared_key(&kp2, &kp1.public_key());
assert_eq!(&shared_key.0[..], &their_precomputed_key.0[..]);
let precomputed_key = diffie_hellman(sk1, pk2, |shared_secret| {
let mut buffer = [0; 32];
cryptoxide::salsa20::hsalsa20(shared_secret, &[0; 16], &mut buffer);
buffer
});
assert_eq!(&precomputed_key[..], &their_precomputed_key.0[..]);
}
}