use std::fs;
use std::net::{IpAddr, SocketAddr};
use std::path::Path;
use cid::Cid;
use libp2p_identity::PeerId;
use crate::error::{Error, Result};
use crate::{Did, Document, EncryptionKey, MaError, SigningKey, VerificationMethod};
#[derive(Debug, Clone)]
pub struct GeneratedIdentity {
pub subject_url: Did,
pub document: Document,
pub signing_private_key_hex: String,
pub encryption_private_key_hex: String,
}
fn build_identity(ipns: &str) -> Result<GeneratedIdentity> {
let sign_url = Did::new_url(ipns, None::<String>).map_err(Error::Validation)?;
let enc_url = Did::new_url(ipns, None::<String>).map_err(Error::Validation)?;
let signing_key = SigningKey::generate(sign_url).map_err(Error::Validation)?;
let encryption_key = EncryptionKey::generate(enc_url).map_err(Error::Validation)?;
build_identity_from_keys(ipns, &signing_key, &encryption_key)
}
pub(crate) fn build_identity_from_keys(
ipns: &str,
signing_key: &SigningKey,
encryption_key: &EncryptionKey,
) -> Result<GeneratedIdentity> {
let subject_url = Did::new_identity(ipns).map_err(Error::Validation)?;
let mut document = Document::new(&subject_url, &subject_url);
let assertion_vm = VerificationMethod::new(
subject_url.base_id(),
subject_url.base_id(),
signing_key.key_type.clone(),
"sign",
signing_key.public_key_multibase.clone(),
)
.map_err(Error::Validation)?;
let key_agreement_vm = VerificationMethod::new(
subject_url.base_id(),
subject_url.base_id(),
encryption_key.key_type.clone(),
"enc",
encryption_key.public_key_multibase.clone(),
)
.map_err(Error::Validation)?;
let assertion_vm_id = assertion_vm.id.clone();
document
.add_verification_method(assertion_vm.clone())
.map_err(Error::Validation)?;
document
.add_verification_method(key_agreement_vm.clone())
.map_err(Error::Validation)?;
document.assertion_method = vec![assertion_vm_id];
document.key_agreement = vec![key_agreement_vm.id.clone()];
document
.sign(signing_key, &assertion_vm)
.map_err(Error::Validation)?;
Ok(GeneratedIdentity {
subject_url,
document,
signing_private_key_hex: hex::encode(signing_key.private_key_bytes()),
encryption_private_key_hex: hex::encode(encryption_key.private_key_bytes()),
})
}
pub fn ipns_from_secret(secret: [u8; 32]) -> Result<String> {
let keypair = libp2p_identity::Keypair::ed25519_from_bytes(secret)
.map_err(|_| Error::Validation(MaError::InvalidIdentitySecret))?;
let peer_id = PeerId::from_public_key(&keypair.public());
let cid = Cid::new_v1(0x72, peer_id.into());
Ok(multibase::encode(
multibase::Base::Base36Lower,
cid.to_bytes(),
))
}
pub fn generate_identity(ipns: &str) -> Result<GeneratedIdentity> {
build_identity(ipns)
}
pub fn generate_identity_from_secret(secret: [u8; 32]) -> Result<GeneratedIdentity> {
let ipns = ipns_from_secret(secret)?;
build_identity(&ipns)
}
pub fn load_secret_key_bytes(path: &Path) -> Result<Option<[u8; 32]>> {
if !path.exists() {
return Ok(None);
}
let bytes = fs::read(path).map_err(|e| Error::SecretKey(e.to_string()))?;
let key_bytes: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| Error::SecretKey(format!("invalid key file length in {}", path.display())))?;
Ok(Some(key_bytes))
}
pub fn generate_secret_key_file(path: &Path) -> Result<[u8; 32]> {
if path.exists() {
return Err(Error::SecretKey(format!(
"secret key already exists at {}",
path.display()
)));
}
let mut key_bytes = [0u8; 32];
use rand::RngCore;
rand::rngs::OsRng.fill_bytes(&mut key_bytes);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
Error::SecretKey(format!("failed to create dir {}: {}", parent.display(), e))
})?;
}
fs::write(path, key_bytes)
.map_err(|e| Error::SecretKey(format!("failed to write {}: {}", path.display(), e)))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o400));
}
Ok(key_bytes)
}
pub fn socket_addr_to_multiaddr(addr: &SocketAddr) -> String {
match addr.ip() {
IpAddr::V4(ip) => format!("/ip4/{}/udp/{}/quic-v1", ip, addr.port()),
IpAddr::V6(ip) => format!("/ip6/{}/udp/{}/quic-v1", ip, addr.port()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::path::PathBuf;
fn test_tmp_file(name: &str) -> PathBuf {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tmp")
.join("identity-tests");
fs::create_dir_all(&root).expect("failed creating test tmp directory");
root.join(name)
}
#[test]
fn multiaddr_ipv4() {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4433);
assert_eq!(
socket_addr_to_multiaddr(&addr),
"/ip4/127.0.0.1/udp/4433/quic-v1"
);
}
#[test]
fn multiaddr_ipv6() {
let addr = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 5555);
assert_eq!(socket_addr_to_multiaddr(&addr), "/ip6/::1/udp/5555/quic-v1");
}
#[test]
fn load_missing_returns_none() {
let path = test_tmp_file("nonexistent-key");
let _ = fs::remove_file(&path);
assert!(load_secret_key_bytes(&path).unwrap().is_none());
}
#[test]
fn generate_and_load_round_trip() {
let path = test_tmp_file("round-trip-key");
let _ = fs::remove_file(&path);
let generated = generate_secret_key_file(&path).unwrap();
let loaded = load_secret_key_bytes(&path).unwrap().unwrap();
assert_eq!(generated, loaded);
let _ = fs::remove_file(&path);
}
#[test]
fn generate_refuses_overwrite() {
let path = test_tmp_file("no-overwrite-key");
let _ = fs::remove_file(&path);
generate_secret_key_file(&path).unwrap();
let err = generate_secret_key_file(&path).unwrap_err();
assert!(matches!(err, crate::error::Error::SecretKey(_)));
let _ = fs::remove_file(&path);
}
}