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#[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 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 pub fn from_secret_bytes(bytes: [u8; 32]) -> Self {
37 Self {
38 signing: SigningKey::from_bytes(&bytes),
39 }
40 }
41
42 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 pub fn secret_bytes(&self) -> [u8; 32] {
59 self.signing.to_bytes()
60 }
61
62 pub fn secret_hex(&self) -> String {
64 hex::encode(self.secret_bytes())
65 }
66
67 pub fn public_bytes(&self) -> [u8; 32] {
69 self.signing.verifying_key().to_bytes()
70 }
71
72 pub fn public_hex(&self) -> String {
74 hex::encode(self.public_bytes())
75 }
76
77 pub(crate) fn scalar_bytes(&self) -> [u8; 32] {
79 self.signing.to_scalar_bytes()
80 }
81
82 pub fn sign_hex(&self, message: &[u8]) -> String {
84 hex::encode(self.signing.sign(message).to_bytes())
85 }
86
87 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 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 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}