pub fn ed25519_multibase_pubkey(public_key_bytes: &[u8; 32]) -> String {
let mut buf = Vec::with_capacity(34);
buf.extend_from_slice(&[0xed, 0x01]);
buf.extend_from_slice(public_key_bytes);
multibase::encode(multibase::Base::Base58Btc, &buf)
}
const ED25519_PUB_CODEC: [u8; 2] = [0xed, 0x01];
pub fn decode_ed25519_public_key_multibase(mb: &str) -> Result<[u8; 32], DidKeyError> {
let (_, raw) = multibase::decode(mb).map_err(|e| DidKeyError::Multibase(e.to_string()))?;
let key_bytes = if raw.len() >= 2 && [raw[0], raw[1]] == ED25519_PUB_CODEC {
&raw[2..]
} else {
&raw[..]
};
key_bytes
.try_into()
.map_err(|_| DidKeyError::InvalidSeedLength)
}
const ED25519_PRIV_CODEC: [u8; 2] = [0x80, 0x26]; const X25519_PRIV_CODEC: [u8; 2] = [0x82, 0x26]; const P256_PRIV_CODEC: [u8; 2] = [0x86, 0x26];
pub fn decode_private_key_multibase(mb: &str) -> Result<[u8; 32], DidKeyError> {
let (_, raw) = multibase::decode(mb).map_err(|e| DidKeyError::Multibase(e.to_string()))?;
let key_bytes = if raw.len() >= 2 {
match [raw[0], raw[1]] {
ED25519_PRIV_CODEC | X25519_PRIV_CODEC | P256_PRIV_CODEC => &raw[2..],
_ => &raw[..],
}
} else {
&raw[..]
};
key_bytes
.try_into()
.map_err(|_| DidKeyError::InvalidSeedLength)
}
#[cfg(feature = "didcomm")]
pub struct DidKeySecrets {
pub signing: affinidi_tdk::secrets_resolver::secrets::Secret,
pub key_agreement: affinidi_tdk::secrets_resolver::secrets::Secret,
}
#[cfg(feature = "didcomm")]
pub fn secrets_from_did_key(did: &str, seed: &[u8; 32]) -> Result<DidKeySecrets, DidKeyError> {
use affinidi_tdk::secrets_resolver::secrets::Secret;
let ed_pub_mb = did
.strip_prefix("did:key:")
.ok_or(DidKeyError::InvalidDidKey)?;
let mut signing = Secret::generate_ed25519(None, Some(seed));
signing.id = format!("{did}#{ed_pub_mb}");
let mut key_agreement = signing
.to_x25519()
.map_err(|e| DidKeyError::X25519Conversion(e.to_string()))?;
let x_pub_mb = key_agreement
.get_public_keymultibase()
.map_err(|e| DidKeyError::X25519Conversion(e.to_string()))?;
key_agreement.id = format!("{did}#{x_pub_mb}");
Ok(DidKeySecrets {
signing,
key_agreement,
})
}
#[derive(Debug)]
pub enum DidKeyError {
Multibase(String),
InvalidSeedLength,
InvalidDidKey,
#[cfg(feature = "didcomm")]
X25519Conversion(String),
}
impl std::fmt::Display for DidKeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Multibase(e) => write!(f, "invalid private key multibase: {e}"),
Self::InvalidSeedLength => write!(f, "private key seed must be 32 bytes"),
Self::InvalidDidKey => write!(f, "invalid did:key format"),
#[cfg(feature = "didcomm")]
Self::X25519Conversion(e) => write!(f, "X25519 conversion failed: {e}"),
}
}
}
impl std::error::Error for DidKeyError {}
#[cfg(feature = "client")]
pub fn secret_from_key_response(
resp: &crate::client::GetKeySecretResponse,
) -> Result<affinidi_tdk::secrets_resolver::secrets::Secret, DidKeyError> {
affinidi_tdk::secrets_resolver::secrets::Secret::from_multibase(
&resp.private_key_multibase,
None,
)
.map_err(|e| DidKeyError::Multibase(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ed25519_multibase_pubkey_format() {
let key = [0u8; 32];
let result = ed25519_multibase_pubkey(&key);
assert!(result.starts_with('z'));
let (_, decoded) = multibase::decode(&result).unwrap();
assert_eq!(decoded.len(), 34);
assert_eq!(decoded[0], 0xed);
assert_eq!(decoded[1], 0x01);
assert_eq!(&decoded[2..], &key);
}
#[test]
fn test_decode_private_key_multibase_roundtrip() {
let seed = [42u8; 32];
let encoded = multibase::encode(multibase::Base::Base58Btc, seed);
let decoded = decode_private_key_multibase(&encoded).unwrap();
assert_eq!(decoded, seed);
}
#[test]
fn test_decode_private_key_multibase_with_codec_prefix() {
let seed = [42u8; 32];
let mut prefixed = Vec::with_capacity(34);
prefixed.extend_from_slice(&ED25519_PRIV_CODEC);
prefixed.extend_from_slice(&seed);
let encoded = multibase::encode(multibase::Base::Base58Btc, &prefixed);
let decoded = decode_private_key_multibase(&encoded).unwrap();
assert_eq!(decoded, seed);
}
#[test]
fn test_decode_private_key_multibase_invalid() {
let result = decode_private_key_multibase("!!!bad!!!");
assert!(result.is_err());
}
#[test]
fn test_decode_private_key_multibase_wrong_length() {
let encoded = multibase::encode(multibase::Base::Base58Btc, [1u8; 16]);
let result = decode_private_key_multibase(&encoded);
assert!(matches!(result, Err(DidKeyError::InvalidSeedLength)));
}
#[cfg(feature = "didcomm")]
#[test]
fn test_secrets_from_did_key_uses_multibase_fragment_ids() {
use affinidi_tdk::secrets_resolver::secrets::Secret;
let seed = [42u8; 32];
let ed_secret = Secret::generate_ed25519(None, Some(&seed));
let ed_pub_mb = ed_secret.get_public_keymultibase().unwrap();
let did = format!("did:key:{ed_pub_mb}");
let secrets = secrets_from_did_key(&did, &seed).expect("did:key secrets");
assert_eq!(secrets.signing.id, format!("{did}#{ed_pub_mb}"));
assert_ne!(secrets.signing.id, format!("{did}#key-0"));
assert!(
secrets.key_agreement.id.starts_with(&format!("{did}#z")),
"key_agreement.id should start with `{did}#z`, got: {}",
secrets.key_agreement.id
);
assert_ne!(secrets.key_agreement.id, format!("{did}#key-1"));
assert_ne!(secrets.key_agreement.id, secrets.signing.id);
}
#[cfg(feature = "didcomm")]
#[test]
fn test_secrets_from_did_key_is_deterministic() {
use affinidi_tdk::secrets_resolver::secrets::Secret;
let seed = [7u8; 32];
let ed_secret = Secret::generate_ed25519(None, Some(&seed));
let did = format!("did:key:{}", ed_secret.get_public_keymultibase().unwrap());
let a = secrets_from_did_key(&did, &seed).unwrap();
let b = secrets_from_did_key(&did, &seed).unwrap();
assert_eq!(a.signing.id, b.signing.id);
assert_eq!(a.key_agreement.id, b.key_agreement.id);
}
#[cfg(feature = "didcomm")]
#[test]
fn test_secrets_from_did_key_rejects_non_did_key() {
let seed = [1u8; 32];
let result = secrets_from_did_key("did:webvh:abc:example.com:vta", &seed);
assert!(matches!(result, Err(DidKeyError::InvalidDidKey)));
}
}