Skip to main content

nysiris_core/
identity.rs

1use std::fmt;
2
3use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
4use rand::RngCore;
5
6use crate::crypto::{open_dm, seal_dm, SealedDm};
7use crate::error::{CoreError, Result};
8
9/// The chat identity: an ed25519 keypair whose public key is the stable chat
10/// id. This is separate from the Nym transport identity.
11#[derive(Clone)]
12pub struct ChatIdentity {
13    signing: SigningKey,
14}
15
16impl fmt::Debug for ChatIdentity {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        f.debug_struct("ChatIdentity")
19            .field("public_hex", &self.public_hex())
20            .finish_non_exhaustive()
21    }
22}
23
24impl ChatIdentity {
25    /// Generate a new identity from the operating-system RNG.
26    pub fn generate() -> Self {
27        let mut bytes = [0u8; 32];
28        rand::rngs::OsRng.fill_bytes(&mut bytes);
29        Self::from_secret_bytes(bytes)
30    }
31
32    /// Restore an identity from raw ed25519 secret bytes.
33    ///
34    /// The bytes are the encryption key material: never log them or store them
35    /// unencrypted.
36    pub fn from_secret_bytes(bytes: [u8; 32]) -> Self {
37        Self {
38            signing: SigningKey::from_bytes(&bytes),
39        }
40    }
41
42    /// Restore an identity from a 32-byte hex secret.
43    pub fn from_secret_hex(value: &str) -> Result<Self> {
44        let raw = hex::decode(value.trim())
45            .map_err(|_| CoreError::InvalidKey("secret key is not hex".into()))?;
46        if raw.len() != 32 {
47            return Err(CoreError::InvalidKey(format!(
48                "secret key must be 32 bytes, got {}",
49                raw.len()
50            )));
51        }
52        let mut bytes = [0u8; 32];
53        bytes.copy_from_slice(&raw);
54        Ok(Self::from_secret_bytes(bytes))
55    }
56
57    /// Secret serialization. Callers must encrypt it at rest.
58    pub fn secret_bytes(&self) -> [u8; 32] {
59        self.signing.to_bytes()
60    }
61
62    /// Secret serialization as lowercase hex. Callers must encrypt it at rest.
63    pub fn secret_hex(&self) -> String {
64        hex::encode(self.secret_bytes())
65    }
66
67    /// The ed25519 public key bytes, which are also the stable chat id.
68    pub fn public_bytes(&self) -> [u8; 32] {
69        self.signing.verifying_key().to_bytes()
70    }
71
72    /// The ed25519 public key as lowercase hex.
73    pub fn public_hex(&self) -> String {
74        hex::encode(self.public_bytes())
75    }
76
77    /// Curve25519 scalar bytes for X25519 DH, derived from the ed25519 secret.
78    pub(crate) fn scalar_bytes(&self) -> [u8; 32] {
79        self.signing.to_scalar_bytes()
80    }
81
82    /// Sign a message with this identity's ed25519 key, returning hex.
83    pub fn sign_hex(&self, message: &[u8]) -> String {
84        hex::encode(self.signing.sign(message).to_bytes())
85    }
86
87    /// Verify a hex-encoded ed25519 signature against a hex public key.
88    ///
89    /// Uses strict verification, so non-canonical signatures are rejected.
90    pub fn verify_hex(public_hex: &str, message: &[u8], signature_hex: &str) -> Result<()> {
91        let key_bytes = crate::ids::pubkey_bytes(public_hex)?;
92        let key = VerifyingKey::from_bytes(&key_bytes)
93            .map_err(|e| CoreError::InvalidKey(format!("bad ed25519 point: {e}")))?;
94        let sig_bytes = hex::decode(signature_hex.trim())
95            .map_err(|_| CoreError::Crypto("signature is not hex".into()))?;
96        if sig_bytes.len() != 64 {
97            return Err(CoreError::Crypto(format!(
98                "signature must be 64 bytes, got {}",
99                sig_bytes.len()
100            )));
101        }
102        let mut sig = [0u8; 64];
103        sig.copy_from_slice(&sig_bytes);
104        key.verify_strict(message, &Signature::from_bytes(&sig))
105            .map_err(|e| CoreError::Crypto(format!("bad signature: {e}")))
106    }
107
108    /// Seal `plaintext` to a peer's ed25519 public key.
109    pub fn seal_to(
110        &self,
111        recipient_pub_hex: &str,
112        aad: &[u8],
113        plaintext: &[u8],
114    ) -> Result<SealedDm> {
115        seal_dm(recipient_pub_hex, aad, plaintext)
116    }
117
118    /// Open a sealed blob addressed to this identity.
119    pub fn open_sealed(&self, sealed: &SealedDm, aad: &[u8]) -> Result<Vec<u8>> {
120        open_dm(self, sealed, aad)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn identity_round_trips_and_signs() {
130        let id = ChatIdentity::from_secret_bytes([9u8; 32]);
131        let restored = ChatIdentity::from_secret_hex(&id.secret_hex()).unwrap();
132        assert_eq!(id.public_hex(), restored.public_hex());
133        let sig = id.sign_hex(b"hello");
134        ChatIdentity::verify_hex(&id.public_hex(), b"hello", &sig).unwrap();
135        assert!(ChatIdentity::verify_hex(&id.public_hex(), b"hello!", &sig).is_err());
136    }
137}