[WIP] 0.3.0 mtp update
This commit is contained in:
parent
7dc98ef29b
commit
e1dd86ec02
42 changed files with 2422 additions and 1429 deletions
|
|
@ -5,7 +5,7 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
iota-paths = { path = "../iota-paths" }
|
||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
|
||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
|
||||
"crypto"
|
||||
] }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,50 @@
|
|||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use mtp::crypto::{EncryptionType, Keyring, PublicKeyBundle, decrypt_with, encrypt_for};
|
||||
use mtp::crypto::{
|
||||
EncryptionType, Keyring, MultiEncryptedMessage, PublicKeyBundle, decrypt_multi_for,
|
||||
encrypt_multi_for,
|
||||
};
|
||||
|
||||
const CHALLENGE_PURPOSE: u8 = 0x01;
|
||||
const LEGACY_AAD_DOMAIN: &[u8] = b"IOTA-MTP-AAD-1";
|
||||
|
||||
fn bind_aad(plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, String> {
|
||||
let aad_len = u32::try_from(aad.len())
|
||||
.map_err(|_| "associated data is too large to encode".to_string())?;
|
||||
let mut bound = Vec::with_capacity(
|
||||
LEGACY_AAD_DOMAIN
|
||||
.len()
|
||||
.saturating_add(4)
|
||||
.saturating_add(aad.len())
|
||||
.saturating_add(plaintext.len()),
|
||||
);
|
||||
bound.extend_from_slice(LEGACY_AAD_DOMAIN);
|
||||
bound.extend_from_slice(&aad_len.to_be_bytes());
|
||||
bound.extend_from_slice(aad);
|
||||
bound.extend_from_slice(plaintext);
|
||||
Ok(bound)
|
||||
}
|
||||
|
||||
fn unbind_aad(bound: &[u8], aad: &[u8]) -> Result<Vec<u8>, String> {
|
||||
let header_len = LEGACY_AAD_DOMAIN.len() + 4;
|
||||
if bound.len() < header_len || &bound[..LEGACY_AAD_DOMAIN.len()] != LEGACY_AAD_DOMAIN {
|
||||
return Err("associated-data binding is invalid".to_string());
|
||||
}
|
||||
let length_start = LEGACY_AAD_DOMAIN.len();
|
||||
let length_end = length_start + 4;
|
||||
let aad_len = u32::from_be_bytes(
|
||||
bound[length_start..length_end]
|
||||
.try_into()
|
||||
.map_err(|_| "associated-data length is invalid".to_string())?,
|
||||
) as usize;
|
||||
let aad_start = length_end;
|
||||
let aad_end = aad_start
|
||||
.checked_add(aad_len)
|
||||
.ok_or_else(|| "associated-data length overflows".to_string())?;
|
||||
if aad_end > bound.len() || &bound[aad_start..aad_end] != aad {
|
||||
return Err("associated data does not match".to_string());
|
||||
}
|
||||
Ok(bound[aad_end..].to_vec())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum DataFormat {
|
||||
|
|
@ -13,17 +58,25 @@ pub fn encrypt(
|
|||
aad: &[u8],
|
||||
recipient_pub_key_bundle: &PublicKeyBundle,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
encrypt_for(
|
||||
let bound_plaintext = bind_aad(plaintext, aad)?;
|
||||
let encrypted = encrypt_multi_for(
|
||||
EncryptionType::MlKemChaCha20Poly1305,
|
||||
recipient_pub_key_bundle,
|
||||
plaintext,
|
||||
aad,
|
||||
CHALLENGE_PURPOSE,
|
||||
&bound_plaintext,
|
||||
std::slice::from_ref(recipient_pub_key_bundle),
|
||||
)
|
||||
.map_err(|e| format!("encryption error: {:?}", e))
|
||||
.map_err(|e| format!("encryption error: {e:?}"))?;
|
||||
encrypted
|
||||
.to_bytes()
|
||||
.map_err(|e| format!("encryption encoding error: {e:?}"))
|
||||
}
|
||||
|
||||
pub fn decrypt(ciphertext: &[u8], aad: &[u8], keyring: &Keyring) -> Result<Vec<u8>, String> {
|
||||
decrypt_with(ciphertext, keyring, aad).map_err(|e| format!("decryption error: {:?}", e))
|
||||
let message = MultiEncryptedMessage::from_bytes(ciphertext)
|
||||
.map_err(|e| format!("decryption envelope error: {e:?}"))?;
|
||||
let bound_plaintext = decrypt_multi_for(&message, CHALLENGE_PURPOSE, keyring)
|
||||
.map_err(|e| format!("decryption error: {e:?}"))?;
|
||||
unbind_aad(&bound_plaintext, aad)
|
||||
}
|
||||
|
||||
pub fn encrypt_challenge(
|
||||
|
|
@ -51,3 +104,18 @@ pub fn export(data: &[u8], format: DataFormat) -> Result<String, String> {
|
|||
DataFormat::Hex => Ok(hex::encode(data)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn encrypt_decrypt_binds_associated_data() -> Result<(), String> {
|
||||
let keyring = Keyring::generate();
|
||||
let ciphertext = encrypt(b"challenge", b"context", &keyring.public_key_bundle())?;
|
||||
|
||||
assert_eq!(decrypt(&ciphertext, b"context", &keyring)?, b"challenge");
|
||||
assert!(decrypt(&ciphertext, b"other-context", &keyring).is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ pub fn delete_user_directory(user_id: i64) -> io::Result<()> {
|
|||
}
|
||||
|
||||
pub fn credential_path(user_id: i64) -> PathBuf {
|
||||
storage_directory().join("credentials").join(format!("{user_id}.tu"))
|
||||
storage_directory()
|
||||
.join("credentials")
|
||||
.join(format!("{user_id}.tu"))
|
||||
}
|
||||
|
||||
pub fn read_user_credential(user_id: i64) -> io::Result<Option<String>> {
|
||||
|
|
@ -58,7 +60,10 @@ pub fn read_user_credential(user_id: i64) -> io::Result<Option<String>> {
|
|||
|
||||
/// Resolve a credential by immutable account id. A valid legacy
|
||||
/// `<username>.tu` is migrated atomically the first time it is encountered.
|
||||
pub fn read_user_credential_with_legacy(user_id: i64, username: &str) -> io::Result<Option<String>> {
|
||||
pub fn read_user_credential_with_legacy(
|
||||
user_id: i64,
|
||||
username: &str,
|
||||
) -> io::Result<Option<String>> {
|
||||
if let Some(credential) = read_user_credential(user_id)? {
|
||||
return Ok(Some(credential));
|
||||
}
|
||||
|
|
@ -71,7 +76,10 @@ pub fn read_user_credential_with_legacy(user_id: i64, username: &str) -> io::Res
|
|||
let parsed = crate::tu::TuCredential::parse(&credential)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||
if parsed.user_id != user_id {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "legacy credential user id mismatch"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"legacy credential user id mismatch",
|
||||
));
|
||||
}
|
||||
write_user_credential(user_id, &parsed.to_canonical_string())?;
|
||||
fs::remove_file(legacy)?;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
pub mod crypto_helper;
|
||||
pub mod crypto_util;
|
||||
pub mod file_util;
|
||||
pub mod mtp_compat;
|
||||
pub mod route_target;
|
||||
pub mod tu;
|
||||
|
|
|
|||
67
iota-util/src/mtp_compat.rs
Normal file
67
iota-util/src/mtp_compat.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use mtp::codec::{CommunicationValue, DataValue};
|
||||
use mtp::type_map::DataTypeId;
|
||||
|
||||
/*
|
||||
* Keep legacy control-plane handlers source-compatible while they migrate to
|
||||
* MTP's explicit optional routing fields. Relay handlers must use sender() and
|
||||
* receiver() directly so an absent outer sender cannot become an identity.
|
||||
*/
|
||||
pub trait CommunicationValueCompat {
|
||||
fn get_id(&self) -> u32;
|
||||
fn get_sender(&self) -> u64;
|
||||
fn get_receiver(&self) -> u64;
|
||||
}
|
||||
|
||||
impl CommunicationValueCompat for CommunicationValue {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_sender(&self) -> u64 {
|
||||
self.sender().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_receiver(&self) -> u64 {
|
||||
self.receiver().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OptionalDataValueExt<'a> {
|
||||
fn as_bool(self) -> Option<bool>;
|
||||
fn as_str(self) -> Option<&'a str>;
|
||||
fn as_string(self) -> Option<String>;
|
||||
fn as_number(self) -> Option<i128>;
|
||||
fn as_signed_number(self) -> Option<i128>;
|
||||
fn as_array(self) -> Option<Vec<DataValue>>;
|
||||
fn as_container(self) -> Option<Vec<(DataTypeId, DataValue)>>;
|
||||
}
|
||||
|
||||
impl<'a> OptionalDataValueExt<'a> for Option<&'a DataValue> {
|
||||
fn as_bool(self) -> Option<bool> {
|
||||
self.and_then(DataValue::as_bool)
|
||||
}
|
||||
|
||||
fn as_str(self) -> Option<&'a str> {
|
||||
self.and_then(DataValue::as_str)
|
||||
}
|
||||
|
||||
fn as_string(self) -> Option<String> {
|
||||
self.and_then(DataValue::as_string)
|
||||
}
|
||||
|
||||
fn as_number(self) -> Option<i128> {
|
||||
self.and_then(DataValue::as_number)
|
||||
}
|
||||
|
||||
fn as_signed_number(self) -> Option<i128> {
|
||||
self.and_then(DataValue::as_signed_number)
|
||||
}
|
||||
|
||||
fn as_array(self) -> Option<Vec<DataValue>> {
|
||||
self.and_then(DataValue::as_array)
|
||||
}
|
||||
|
||||
fn as_container(self) -> Option<Vec<(DataTypeId, DataValue)>> {
|
||||
self.and_then(DataValue::as_container)
|
||||
}
|
||||
}
|
||||
44
iota-util/src/route_target.rs
Normal file
44
iota-util/src/route_target.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const TARGET_KIND_MASK: u64 = 0xC000_0000_0000_0000;
|
||||
const TARGET_ID_MASK: u64 = (1_u64 << 48) - 1;
|
||||
const USER_TARGET_KIND: u64 = 0x4000_0000_0000_0000;
|
||||
const IOTA_TARGET_KIND: u64 = 0x8000_0000_0000_0000;
|
||||
|
||||
/*
|
||||
* Relay receivers carry their namespace in the wire identity. This prevents
|
||||
* a user ID and an Iota ID with the same numeric value from selecting the
|
||||
* wrong connection at an Omikron.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RouteTarget {
|
||||
User(u64),
|
||||
Iota(u64),
|
||||
}
|
||||
|
||||
impl RouteTarget {
|
||||
pub fn wire_id(self) -> Option<u64> {
|
||||
let (kind, id) = match self {
|
||||
Self::User(id) => (USER_TARGET_KIND, id),
|
||||
Self::Iota(id) => (IOTA_TARGET_KIND, id),
|
||||
};
|
||||
(id > 0 && id <= TARGET_ID_MASK).then_some(kind | id)
|
||||
}
|
||||
|
||||
pub fn from_wire_id(value: u64) -> Option<Self> {
|
||||
let id = value & TARGET_ID_MASK;
|
||||
if id == 0 || value & !(TARGET_KIND_MASK | TARGET_ID_MASK) != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
match value & TARGET_KIND_MASK {
|
||||
USER_TARGET_KIND => Some(Self::User(id)),
|
||||
IOTA_TARGET_KIND => Some(Self::Iota(id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn id(self) -> u64 {
|
||||
match self {
|
||||
Self::User(id) | Self::Iota(id) => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,10 @@ impl fmt::Debug for TuCredential {
|
|||
|
||||
impl TuCredential {
|
||||
pub fn parse(input: &str) -> Result<Self, TuError> {
|
||||
let (identity, encoded_keyring) = input.trim().split_once("::").ok_or(TuError::InvalidFormat)?;
|
||||
let (identity, encoded_keyring) = input
|
||||
.trim()
|
||||
.split_once("::")
|
||||
.ok_or(TuError::InvalidFormat)?;
|
||||
if encoded_keyring.is_empty() || encoded_keyring.contains("::") {
|
||||
return Err(TuError::InvalidFormat);
|
||||
}
|
||||
|
|
@ -60,7 +63,11 @@ impl TuCredential {
|
|||
return Err(TuError::InvalidUserId);
|
||||
}
|
||||
let keyring = keyring_from_base64(encoded_keyring).ok_or(TuError::InvalidKeyring)?;
|
||||
Ok(Self { user_id, omega_host: omega_host.trim().to_owned(), keyring })
|
||||
Ok(Self {
|
||||
user_id,
|
||||
omega_host: omega_host.trim().to_owned(),
|
||||
keyring,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn public_key_bundle(&self) -> PublicKeyBundle {
|
||||
|
|
@ -68,7 +75,12 @@ impl TuCredential {
|
|||
}
|
||||
|
||||
pub fn to_canonical_string(&self) -> String {
|
||||
format!("{}@{}::{}", self.user_id, self.omega_host, keyring_to_base64(&self.keyring))
|
||||
format!(
|
||||
"{}@{}::{}",
|
||||
self.user_id,
|
||||
self.omega_host,
|
||||
keyring_to_base64(&self.keyring)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,16 +91,31 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn round_trip_is_canonical() {
|
||||
let credential = TuCredential { user_id: 42, omega_host: "omega.example:443".into(), keyring: generate_keyring() };
|
||||
let credential = TuCredential {
|
||||
user_id: 42,
|
||||
omega_host: "omega.example:443".into(),
|
||||
keyring: generate_keyring(),
|
||||
};
|
||||
let parsed = TuCredential::parse(&credential.to_canonical_string()).unwrap();
|
||||
assert_eq!(parsed.user_id, 42);
|
||||
assert_eq!(parsed.omega_host, "omega.example:443");
|
||||
assert_eq!(parsed.to_canonical_string(), credential.to_canonical_string());
|
||||
assert_eq!(
|
||||
parsed.to_canonical_string(),
|
||||
credential.to_canonical_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_credentials() {
|
||||
for value in ["", "1@omega", "@omega::abc", "0@omega::abc", "281474976710656@omega::abc", "1@::abc", "1@omega::abc::def"] {
|
||||
for value in [
|
||||
"",
|
||||
"1@omega",
|
||||
"@omega::abc",
|
||||
"0@omega::abc",
|
||||
"281474976710656@omega::abc",
|
||||
"1@::abc",
|
||||
"1@omega::abc::def",
|
||||
] {
|
||||
assert!(TuCredential::parse(value).is_err(), "{value}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue