ng_repo/
utils.rs

1// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
2// All rights reserved.
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
5// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6// at your option. All files in the project carrying such
7// notice may not be copied, modified, or distributed except
8// according to those terms.
9
10use chacha20::cipher::{KeyIvInit, StreamCipher};
11use chacha20::ChaCha20;
12use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
13use ed25519_dalek::*;
14use futures::channel::mpsc;
15use rand::rngs::OsRng;
16use rand::RngCore;
17use time::{OffsetDateTime, UtcOffset};
18use web_time::{Duration, SystemTime, UNIX_EPOCH};
19use zeroize::Zeroize;
20
21use crate::errors::*;
22#[allow(unused_imports)]
23use crate::log::*;
24use crate::types::*;
25
26pub fn derive_key(context: &str, key_material: &[u8]) -> [u8; 32] {
27    blake3::derive_key(context, key_material)
28}
29
30pub fn ed_keypair_from_priv_bytes(secret_key: [u8; 32]) -> (PrivKey, PubKey) {
31    let sk = SecretKey::from_bytes(&secret_key).unwrap();
32    let pk: PublicKey = (&sk).into();
33    let pub_key = PubKey::Ed25519PubKey(pk.to_bytes());
34    let priv_key = PrivKey::Ed25519PrivKey(secret_key);
35    (priv_key, pub_key)
36}
37
38pub fn from_ed_privkey_to_dh_privkey(private: &PrivKey) -> PrivKey {
39    //SecretKey and ExpandedSecretKey are Zeroized at drop
40    if let PrivKey::Ed25519PrivKey(slice) = private {
41        let ed25519_priv = SecretKey::from_bytes(slice).unwrap();
42        let exp: ExpandedSecretKey = (&ed25519_priv).into();
43        let mut exp_bytes = exp.to_bytes();
44        exp_bytes[32..].zeroize();
45        let mut bits = *slice_as_array!(&exp_bytes[0..32], [u8; 32]).unwrap();
46        bits[0] &= 248;
47        bits[31] &= 127;
48        bits[31] |= 64;
49        // PrivKey takes ownership and will zeroize on drop
50        PrivKey::X25519PrivKey(bits)
51    } else {
52        panic!("this is not an Edmonds privkey")
53    }
54}
55
56/// don't forget to zeroize the string later on
57pub fn decode_key(key_string: &str) -> Result<PubKey, NgError> {
58    let mut vec = base64_url::decode(key_string).map_err(|_| NgError::InvalidKey)?;
59    vec.reverse();
60    Ok(serde_bare::from_slice(&vec).map_err(|_| NgError::InvalidKey)?)
61}
62
63pub fn decode_priv_key(key_string: &str) -> Result<PrivKey, NgError> {
64    let mut vec = base64_url::decode(key_string).map_err(|_| NgError::InvalidKey)?;
65    vec.reverse();
66    Ok(serde_bare::from_slice(&vec).map_err(|_| NgError::InvalidKey)?)
67}
68
69pub fn decode_sym_key(key_string: &str) -> Result<SymKey, NgError> {
70    let mut vec = base64_url::decode(key_string).map_err(|_| NgError::InvalidKey)?;
71    vec.reverse();
72    Ok(serde_bare::from_slice(&vec).map_err(|_| NgError::InvalidKey)?)
73}
74
75pub fn decode_digest(key_string: &str) -> Result<crate::types::Digest, NgError> {
76    let mut vec = base64_url::decode(key_string).map_err(|_| NgError::InvalidKey)?;
77    vec.reverse();
78    Ok(serde_bare::from_slice(&vec).map_err(|_| NgError::InvalidKey)?)
79}
80
81pub fn decode_overlayid(id_string: &str) -> Result<OverlayId, NgError> {
82    let mut vec = base64_url::decode(id_string).map_err(|_| NgError::InvalidKey)?;
83    vec.reverse();
84    Ok(serde_bare::from_slice(&vec).map_err(|_| NgError::InvalidKey)?)
85}
86
87pub fn ed_privkey_to_ed_pubkey(privkey: &PrivKey) -> PubKey {
88    // SecretKey is zeroized on drop (3 lines below) se we are safe
89    let sk = SecretKey::from_bytes(privkey.slice()).unwrap();
90    let pk: PublicKey = (&sk).into();
91    PubKey::Ed25519PubKey(pk.to_bytes())
92}
93
94/// use with caution. it should be embedded in a zeroize struct in order to be safe
95pub fn random_key() -> [u8; 32] {
96    let mut sk = [0u8; 32];
97    let mut csprng = OsRng {};
98    csprng.fill_bytes(&mut sk);
99    sk
100}
101
102pub fn generate_null_ed_keypair() -> (PrivKey, PubKey) {
103    // we don't use zeroize because... well, it is already a zeroized privkey ;)
104    let master_key: [u8; 32] = [0; 32];
105    let sk = SecretKey::from_bytes(&master_key).unwrap();
106    let pk: PublicKey = (&sk).into();
107    let priv_key = PrivKey::Ed25519PrivKey(sk.to_bytes());
108    let pub_key = PubKey::Ed25519PubKey(pk.to_bytes());
109    (priv_key, pub_key)
110}
111
112pub fn dh_pubkey_from_ed_pubkey_slice(public: &[u8]) -> PubKey {
113    PubKey::X25519PubKey(dh_pubkey_array_from_ed_pubkey_slice(public))
114}
115
116pub fn dh_pubkey_array_from_ed_pubkey_slice(public: &[u8]) -> X25519PubKey {
117    let mut bits: [u8; 32] = [0u8; 32];
118    bits.copy_from_slice(public);
119    let compressed = CompressedEdwardsY(bits);
120    let ed_point: EdwardsPoint = compressed.decompress().unwrap();
121    //compressed.zeroize();
122    let mon_point = ed_point.to_montgomery();
123    //ed_point.zeroize();
124    let array = mon_point.to_bytes();
125    //mon_point.zeroize();
126    array
127}
128
129pub fn pubkey_privkey_to_keypair(pubkey: &PubKey, privkey: &PrivKey) -> Keypair {
130    match (privkey, pubkey) {
131        (PrivKey::Ed25519PrivKey(sk), PubKey::Ed25519PubKey(pk)) => {
132            let secret = SecretKey::from_bytes(sk).unwrap();
133            let public = PublicKey::from_bytes(pk).unwrap();
134
135            Keypair { secret, public }
136        }
137        (_, _) => panic!("cannot sign with Montgomery keys"),
138    }
139}
140
141pub fn keypair_from_ed(secret: SecretKey, public: PublicKey) -> (PrivKey, PubKey) {
142    let ed_priv_key = secret.to_bytes();
143    let ed_pub_key = public.to_bytes();
144    let pub_key = PubKey::Ed25519PubKey(ed_pub_key);
145    let priv_key = PrivKey::Ed25519PrivKey(ed_priv_key);
146    (priv_key, pub_key)
147}
148
149pub fn sign(
150    author_privkey: &PrivKey,
151    author_pubkey: &PubKey,
152    content: &Vec<u8>,
153) -> Result<Sig, NgError> {
154    let keypair = pubkey_privkey_to_keypair(author_pubkey, author_privkey);
155    let sig_bytes = keypair.sign(content.as_slice()).to_bytes();
156    // log_debug!(
157    //     "XXXX SIGN {:?} {:?} {:?}",
158    //     author_pubkey,
159    //     content.as_slice(),
160    //     sig_bytes
161    // );
162    let mut it = sig_bytes.chunks_exact(32);
163    let mut ss: Ed25519Sig = [[0; 32], [0; 32]];
164    ss[0].copy_from_slice(it.next().unwrap());
165    ss[1].copy_from_slice(it.next().unwrap());
166    Ok(Sig::Ed25519Sig(ss))
167}
168
169pub fn verify(content: &Vec<u8>, sig: Sig, pub_key: PubKey) -> Result<(), NgError> {
170    let pubkey = match pub_key {
171        PubKey::Ed25519PubKey(pk) => pk,
172        _ => panic!("cannot verify with Montgomery keys"),
173    };
174    let pk = PublicKey::from_bytes(&pubkey)?;
175    let sig_bytes = match sig {
176        Sig::Ed25519Sig(ss) => [ss[0], ss[1]].concat(),
177    };
178    let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes)?;
179    Ok(pk.verify_strict(content, &sig)?)
180}
181
182pub fn generate_keypair() -> (PrivKey, PubKey) {
183    let mut csprng = OsRng {};
184    let keypair: Keypair = Keypair::generate(&mut csprng);
185    let ed_priv_key = keypair.secret.to_bytes();
186    let ed_pub_key = keypair.public.to_bytes();
187    let priv_key = PrivKey::Ed25519PrivKey(ed_priv_key);
188    let pub_key = PubKey::Ed25519PubKey(ed_pub_key);
189    (priv_key, pub_key)
190}
191
192pub fn encrypt_in_place(plaintext: &mut Vec<u8>, key: [u8; 32], nonce: [u8; 12]) {
193    let mut cipher = ChaCha20::new(&key.into(), &nonce.into());
194    let mut content_dec_slice = plaintext.as_mut_slice();
195    cipher.apply_keystream(&mut content_dec_slice);
196}
197
198/// returns the NextGraph Timestamp of now.
199pub fn now_timestamp() -> Timestamp {
200    ((SystemTime::now()
201        .duration_since(UNIX_EPOCH)
202        .unwrap()
203        .as_secs()
204        - EPOCH_AS_UNIX_TIMESTAMP)
205        / 60)
206        .try_into()
207        .unwrap()
208}
209
210/// returns a new NextGraph Timestamp equivalent to the duration after now.
211pub fn timestamp_after(duration: Duration) -> Timestamp {
212    (((SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + duration).as_secs()
213        - EPOCH_AS_UNIX_TIMESTAMP)
214        / 60)
215        .try_into()
216        .unwrap()
217}
218
219/// displays the NextGraph Timestamp in UTC.
220#[cfg(not(target_arch = "wasm32"))]
221pub fn display_timestamp(ts: &Timestamp) -> String {
222    let dur =
223        Duration::from_secs(EPOCH_AS_UNIX_TIMESTAMP) + Duration::from_secs(*ts as u64 * 60u64);
224
225    let dt: OffsetDateTime = OffsetDateTime::UNIX_EPOCH + dur;
226
227    dt.format(&time::format_description::parse("[day]/[month]/[year] [hour]:[minute] UTC").unwrap())
228        .unwrap()
229}
230
231/// displays the NextGraph Timestamp in local time for the history (JS)
232pub fn display_timestamp_local(ts: Timestamp) -> String {
233    let dur = Duration::from_secs(EPOCH_AS_UNIX_TIMESTAMP) + Duration::from_secs(ts as u64 * 60u64);
234
235    let dt: OffsetDateTime = OffsetDateTime::UNIX_EPOCH + dur;
236
237    let dt = dt.to_offset(TIMEZONE_OFFSET.clone());
238    dt.format(
239        &time::format_description::parse("[day]/[month]/[year repr:last_two] [hour]:[minute]")
240            .unwrap(),
241    )
242    .unwrap()
243}
244
245use lazy_static::lazy_static;
246lazy_static! {
247    static ref TIMEZONE_OFFSET: UtcOffset = unsafe {
248        time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound);
249        UtcOffset::current_local_offset().unwrap()
250    };
251}
252
253pub(crate) type Receiver<T> = mpsc::UnboundedReceiver<T>;
254
255#[cfg(test)]
256mod test {
257    use crate::{
258        log::*,
259        utils::{display_timestamp_local, now_timestamp},
260    };
261
262    #[test]
263    pub fn test_time() {
264        let time = now_timestamp() + 120; // 2 hours later
265        log_info!("{}", display_timestamp_local(time));
266    }
267
268    #[test]
269    pub fn test_locales() {
270        let list = vec!["C", "c", "aa-bb-cc-dd", "aa-ff_bb.456d"];
271        let res: Vec<String> = list
272            .iter()
273            .filter_map(|lang| {
274                if *lang == "C" || *lang == "c" {
275                    None
276                } else {
277                    let mut split = lang.split('.');
278                    let code = split.next().unwrap();
279                    let code = code.replace("_", "-");
280                    let mut split = code.rsplitn(2, '-');
281                    let country = split.next().unwrap();
282                    Some(match split.next() {
283                        Some(next) => format!("{}-{}", next, country.to_uppercase()),
284                        None => country.to_string(),
285                    })
286                }
287            })
288            .collect();
289        log_debug!("{:?}", res);
290    }
291}