use std::convert::{TryFrom, TryInto};
use std::hash::Hash as StdHash;
use arrayvec::ArrayVec;
use bamboo_rs_core_ed25519_yasmf::entry::is_lipmaa_required;
use bamboo_rs_core_ed25519_yasmf::signature::ED25519_SIGNATURE_SIZE;
use bamboo_rs_core_ed25519_yasmf::YasmfHash;
use ed25519_dalek::ed25519::Signature;
use serde::{Deserialize, Serialize};
use crate::entry::EntrySignedError;
use crate::hash::{Blake3ArrayVec, Hash, HASH_SIZE};
use crate::identity::Author;
use crate::operation::OperationEncoded;
use crate::Validate;
pub const SIGNATURE_SIZE: usize = ED25519_SIGNATURE_SIZE;
type BambooEntry =
bamboo_rs_core_ed25519_yasmf::Entry<ArrayVec<[u8; HASH_SIZE]>, ArrayVec<[u8; SIGNATURE_SIZE]>>;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, StdHash)]
pub struct EntrySigned(String);
impl EntrySigned {
pub fn new(value: &str) -> Result<Self, EntrySignedError> {
let inner = Self(value.to_owned());
inner.validate()?;
Ok(inner)
}
pub fn hash(&self) -> Hash {
Hash::new_from_bytes(self.to_bytes()).unwrap()
}
pub fn author(&self) -> Author {
let entry: BambooEntry = self.into();
Author::try_from(entry.author).unwrap()
}
pub fn signature(&self) -> Signature {
let entry: BambooEntry = self.into();
let array_vec = entry.sig.unwrap().0;
Signature::from_bytes(&array_vec.into_inner().unwrap()).unwrap()
}
pub fn payload_hash(&self) -> Hash {
let bamboo_entry: BambooEntry = self.into();
bamboo_entry.payload_hash.try_into().unwrap()
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn to_bytes(&self) -> Vec<u8> {
hex::decode(&self.0).unwrap()
}
pub fn unsigned_bytes(&self) -> Vec<u8> {
let bytes = self.to_bytes();
let signature_offset = bytes.len() - SIGNATURE_SIZE;
bytes[..signature_offset].into()
}
pub fn size(&self) -> u64 {
self.0.len() as u64 / 2
}
pub fn validate_operation(
&self,
operation_encoded: &OperationEncoded,
) -> Result<(), EntrySignedError> {
let entry: BambooEntry = self.into();
let yasmf_hash: YasmfHash<Blake3ArrayVec> = (&operation_encoded.hash()).to_owned().into();
if yasmf_hash != entry.payload_hash {
return Err(EntrySignedError::OperationHashMismatch);
}
Ok(())
}
}
#[cfg(any(feature = "testing", test))]
impl EntrySigned {
pub fn new_without_validation(value: &str) -> Result<Self, EntrySignedError> {
let inner = Self(value.to_owned());
Ok(inner)
}
}
impl From<&EntrySigned> for BambooEntry {
fn from(signed_entry: &EntrySigned) -> Self {
let entry_bytes = signed_entry.to_bytes();
let entry_ref = entry_bytes.as_slice().try_into().unwrap();
bamboo_rs_core_ed25519_yasmf::entry::into_owned(&entry_ref)
}
}
impl TryFrom<&[u8]> for EntrySigned {
type Error = EntrySignedError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::new(&hex::encode(bytes))
}
}
impl Validate for EntrySigned {
type Error = EntrySignedError;
fn validate(&self) -> Result<(), Self::Error> {
hex::decode(&self.0).map_err(|_| EntrySignedError::InvalidHexEncoding)?;
let entry_bytes = self.to_bytes();
let bamboo_entry: bamboo_rs_core_ed25519_yasmf::Entry<&[u8], &[u8]> =
entry_bytes.as_slice().try_into()?;
if is_lipmaa_required(bamboo_entry.seq_num)
&& bamboo_entry.backlink == bamboo_entry.lipmaa_link
{
return Err(EntrySignedError::BacklinkAndSkiplinkIdentical);
};
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::convert::TryInto;
use proptest::prelude::prop;
use rstest::rstest;
use rstest_reuse::apply;
use crate::entry::{sign_and_encode, Entry, EntrySigned};
use crate::identity::KeyPair;
use crate::operation::OperationEncoded;
use crate::test_utils::constants::{default_fields, DEFAULT_HASH, DEFAULT_PRIVATE_KEY};
use crate::test_utils::fixtures::{
entry_signed_encoded, entry_signed_encoded_unvalidated, key_pair, operation,
operation_encoded, operation_fields, random_hash,
};
use crate::test_utils::templates::many_valid_entries;
#[rstest]
fn test_entry_signed(entry_signed_encoded: EntrySigned, key_pair: KeyPair) {
let verification = KeyPair::verify(
key_pair.public_key(),
&entry_signed_encoded.unsigned_bytes(),
&entry_signed_encoded.signature(),
);
assert!(verification.is_ok(), "{:?}", verification.unwrap_err())
}
#[rstest]
fn test_size(entry_signed_encoded: EntrySigned) {
let size: usize = entry_signed_encoded.size().try_into().unwrap();
assert_eq!(size, entry_signed_encoded.to_bytes().len())
}
#[rstest]
fn test_payload_hash(entry_signed_encoded: EntrySigned, operation_encoded: OperationEncoded) {
let expected_payload_hash = operation_encoded.hash();
assert_eq!(entry_signed_encoded.payload_hash(), expected_payload_hash)
}
#[rstest]
#[case::valid_first_entry(entry_signed_encoded_unvalidated(
1,
1,
None,
None,
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
))]
#[case::valid_entry_with_backlink(entry_signed_encoded_unvalidated(
2,
1,
Some(random_hash()),
None,
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
))]
#[case::valid_entry_with_skiplink_and_backlink(entry_signed_encoded_unvalidated(
13,
1,
Some(random_hash()),
Some(random_hash()),
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
))]
#[case::skiplink_ommitted_when_sam_as_backlink(entry_signed_encoded_unvalidated(
14,
1,
Some(random_hash()),
None,
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
))]
fn validate(#[case] entry_signed_encoded_unvalidated: String) {
assert!(EntrySigned::new(&entry_signed_encoded_unvalidated).is_ok());
}
#[rstest]
#[case::empty_string("", "Bytes to decode had length of 0")]
#[case::invalid_hex_string("123456789Z", "invalid hex encoding in entry")]
#[case::another_invalid_hex_string(":{][[5£$%*(&*££ ++`/.", "invalid hex encoding in entry")]
#[case::seq_number_zero(
entry_signed_encoded_unvalidated(
0,
1,
None,
None,
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
),
"Entry sequence must be larger than 0 but was 0"
)]
#[case::should_not_have_skiplink(
entry_signed_encoded_unvalidated(
1,
1,
None,
Some(random_hash()),
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
),
"Could not decode payload hash DecodeError"
)]
#[case::should_not_have_backlink(
entry_signed_encoded_unvalidated(
1,
1,
Some(random_hash()),
None,
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
),
"Could not decode payload hash DecodeError"
)]
#[case::should_not_have_backlink_or_skiplink(
entry_signed_encoded_unvalidated(
1,
1,
Some(DEFAULT_HASH.parse().unwrap()),
Some(DEFAULT_HASH.parse().unwrap()),
Some(operation(Some(operation_fields(default_fields())), None, None))
,
key_pair(DEFAULT_PRIVATE_KEY)
),
"Could not decode payload hash DecodeError"
)]
#[case::missing_backlink(
entry_signed_encoded_unvalidated(
2,
1,
None,
None,
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
),
"Could not decode backlink yamf hash: DecodeError"
)]
#[case::missing_skiplink(
entry_signed_encoded_unvalidated(
8,
1,
Some(random_hash()),
None,
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
),
"Could not decode backlink yamf hash: DecodeError"
)]
#[case::should_not_include_skiplink(
entry_signed_encoded_unvalidated(
14,
1,
Some(DEFAULT_HASH.parse().unwrap()),
Some(DEFAULT_HASH.parse().unwrap()),
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
),
"Could not decode payload hash DecodeError"
)]
#[case::payload_hash_and_size_missing(
entry_signed_encoded_unvalidated(
14,
1,
Some(random_hash()),
Some(DEFAULT_HASH.parse().unwrap()),
None,
key_pair(DEFAULT_PRIVATE_KEY)
),
"Could not decode payload hash DecodeError"
)]
#[case::skiplink_and_backlink_should_be_unique(
entry_signed_encoded_unvalidated(
13,
1,
Some(DEFAULT_HASH.parse().unwrap()),
Some(DEFAULT_HASH.parse().unwrap()),
Some(operation(Some(operation_fields(default_fields())), None, None)),
key_pair(DEFAULT_PRIVATE_KEY)
),
"backlink and skiplink are identical"
)]
fn correct_errors_on_invalid_entries(
#[case] entry_signed_encoded_unvalidated: String,
#[case] expected_error_message: &str,
) {
assert_eq!(
EntrySigned::new(&entry_signed_encoded_unvalidated)
.unwrap_err()
.to_string(),
expected_error_message
);
}
#[apply(many_valid_entries)]
fn it_hashes(#[case] entry: Entry, key_pair: KeyPair) {
let entry_first_encoded = sign_and_encode(&entry, &key_pair).unwrap();
let mut hash_map = HashMap::new();
let key_value = "Value identified by a hash".to_string();
hash_map.insert(&entry_first_encoded, key_value.clone());
let key_value_retrieved = hash_map.get(&entry_first_encoded).unwrap().to_owned();
assert_eq!(key_value, key_value_retrieved)
}
proptest! {
#[test]
fn non_standard_strings_dont_crash(ref s in "\\PC*") {
let result = EntrySigned::new(s);
assert!(result.is_err())
}
}
proptest! {
#[test]
fn partially_correct_strings_dont_crash(
ref author in "[0-9a-f]{64}|[*]{0}",
ref log_id in prop::num::u64::ANY,
ref seq_num in prop::num::u64::ANY,
ref skiplink in "[0-9a-f]{68}|[*]{0}",
ref backlink in "[0-9a-f]{68}|[*]{0}",
ref payload_size in prop::num::u64::ANY,
ref payload_hash in "[0-9a-f]{68}|[*]{0}",
ref signature in "[0-9a-f]{68}|[*]{0}"
) {
let encoded_entry = "0".to_string()
+ author
+ log_id.to_string().as_str()
+ seq_num.to_string().as_str()
+ skiplink
+ backlink
+ payload_size.to_string().as_str()
+ payload_hash
+ signature;
let result = EntrySigned::new(&encoded_entry);
assert!(result.is_err())
}
}
}