ng_repo/
event.rs

1// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
2// Licensed under the Apache License, Version 2.0
3// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
4// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
5// at your option. All files in the project carrying such
6// notice may not be copied, modified, or distributed except
7// according to those terms.
8
9//! Event, a message sent in the PUB/SUB
10
11use core::fmt;
12use std::sync::Arc;
13
14use chacha20::cipher::{KeyIvInit, StreamCipher};
15use chacha20::ChaCha20;
16use zeroize::Zeroize;
17
18use crate::errors::*;
19use crate::repo::{BranchInfo, Repo};
20use crate::store::Store;
21use crate::types::*;
22use crate::utils::*;
23
24impl fmt::Display for Event {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::V0(v0) => {
28                writeln!(f, "V0")?;
29                writeln!(f, "topic_sig:      {}", v0.topic_sig)?;
30                writeln!(f, "peer_sig:      {}", v0.peer_sig)?;
31                write!(f, "content:  {}", v0.content)?;
32                Ok(())
33            }
34        }
35    }
36}
37
38impl fmt::Display for EventContentV0 {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        writeln!(f, "V0")?;
41        writeln!(f, "topic:      {}", self.topic)?;
42        writeln!(f, "publisher:  {}", self.publisher)?;
43        writeln!(f, "seq:        {}", self.seq)?;
44        writeln!(f, "blocks:     {}", self.blocks.len())?;
45        let mut i = 0;
46        for block in &self.blocks {
47            writeln!(f, "========== {:03}: {}", i, block.id())?;
48            i += 1;
49        }
50        writeln!(f, "file ids:     {}", self.file_ids.len())?;
51        let mut i = 0;
52        for file in &self.file_ids {
53            writeln!(f, "========== {:03}: {}", i, file)?;
54            i += 1;
55        }
56        writeln!(f, "key:  {:?}", self.key)?;
57        Ok(())
58    }
59}
60
61impl Event {
62    pub fn new(
63        publisher: &PrivKey,
64        seq: u64,
65        commit: &Commit,
66        additional_blocks: &Vec<BlockId>,
67        repo: &Repo,
68    ) -> Result<Event, NgError> {
69        Ok(Event::V0(EventV0::new(
70            publisher,
71            seq,
72            commit,
73            additional_blocks,
74            repo,
75        )?))
76    }
77
78    pub fn seq_num(&self) -> u64 {
79        match self {
80            Event::V0(v0) => v0.content.seq,
81        }
82    }
83
84    pub fn topic_id(&self) -> &TopicId {
85        match self {
86            Event::V0(v0) => &v0.content.topic,
87        }
88    }
89
90    pub fn file_ids(&self) -> &Vec<ObjectId> {
91        match self {
92            Event::V0(v0) => &v0.content.file_ids,
93        }
94    }
95
96    pub fn publisher(&self) -> &PeerId {
97        match self {
98            Event::V0(v0) => &v0.content.publisher,
99        }
100    }
101
102    pub fn verify(&self) -> Result<(), NgError> {
103        match self {
104            Event::V0(v0) => v0.verify(),
105        }
106    }
107
108    /// opens an event with the key derived from information kept in Repo.
109    ///
110    /// returns the Commit object and optional list of additional block IDs.
111    /// Those blocks have been added to the storage of store of repo so they can be retrieved.
112    pub fn open_with_info(&self, repo: &Repo, branch: &BranchInfo) -> Result<Commit, NgError> {
113        match self {
114            Self::V0(v0) => v0.open_with_info(repo, branch),
115        }
116    }
117
118    pub fn commit_id(&self) -> ObjectId {
119        match self {
120            Self::V0(v0) => v0.content.blocks[0].id(),
121        }
122    }
123
124    pub fn open(
125        &self,
126        store: &Store,
127        repo_id: &RepoId,
128        branch_id: &BranchId,
129        branch_secret: &ReadCapSecret,
130    ) -> Result<Commit, NgError> {
131        match self {
132            Self::V0(v0) => v0.open(store, repo_id, branch_id, branch_secret, true),
133        }
134    }
135
136    pub fn open_with_body(
137        &self,
138        store: &Store,
139        repo_id: &RepoId,
140        branch_id: &BranchId,
141        branch_secret: &ReadCapSecret,
142        with_body: bool,
143    ) -> Result<Commit, NgError> {
144        match self {
145            Self::V0(v0) => v0.open(store, repo_id, branch_id, branch_secret, with_body),
146        }
147    }
148
149    // pub fn put_blocks<'a>(
150    //     &self,
151    //     overlay: &OverlayId,
152    //     storage: &RwLockWriteGuard<'a, dyn BlockStorage + Send + Sync + 'static>,
153    // ) -> Result<ObjectId, NgError> {
154    //     match self {
155    //         Self::V0(v0) => v0.put_blocks(overlay, storage),
156    //     }
157    // }
158}
159
160impl EventV0 {
161    pub fn verify(&self) -> Result<(), NgError> {
162        let content_ser = serde_bare::to_vec(&self.content)?;
163        verify(&content_ser, self.topic_sig, self.content.topic)?;
164        match self.content.publisher {
165            PeerId::Forwarded(peer_id) => verify(&content_ser, self.peer_sig, peer_id)?,
166            PeerId::ForwardedObfuscated(_) => {
167                panic!("cannot verify an Event with obfuscated publisher")
168            }
169            PeerId::Direct(_) => panic!("direct events are not supported"),
170        }
171        if self.content.blocks.len() < 2 {
172            // an event is always containing a commit, which always has at least 2 blocks (one for the commit content, and one for the commit body)
173            return Err(NgError::MalformedEvent);
174        }
175        Ok(())
176    }
177
178    pub fn derive_key(
179        repo_id: &RepoId,
180        branch_id: &BranchId,
181        branch_secret: &ReadCapSecret,
182        publisher: &PubKey,
183    ) -> [u8; blake3::OUT_LEN] {
184        let mut key_material = match (*repo_id, *branch_id, branch_secret.clone(), *publisher) {
185            (
186                PubKey::Ed25519PubKey(repo),
187                PubKey::Ed25519PubKey(branch),
188                SymKey::ChaCha20Key(branch_sec),
189                PubKey::Ed25519PubKey(publ),
190            ) => [repo, branch, branch_sec, publ].concat(),
191            (_, _, _, _) => panic!("cannot derive key with Montgomery key"),
192        };
193        let res = blake3::derive_key(
194            "NextGraph Event Commit ObjectKey ChaCha20 key",
195            key_material.as_slice(),
196        );
197        key_material.zeroize();
198        res
199    }
200
201    pub fn new(
202        publisher: &PrivKey,
203        seq: u64,
204        commit: &Commit,
205        additional_blocks: &Vec<BlockId>,
206        repo: &Repo,
207    ) -> Result<EventV0, NgError> {
208        let branch_id = commit.branch();
209        let repo_id = repo.id;
210        let store = Arc::clone(&repo.store);
211        let branch = repo.branch(branch_id)?;
212        let topic_id = &branch.topic.unwrap();
213        let topic_priv_key = branch
214            .topic_priv_key
215            .as_ref()
216            .ok_or(NgError::PermissionDenied)?;
217        let publisher_pubkey = publisher.to_pub();
218        let key = Self::derive_key(
219            &repo_id,
220            branch_id,
221            &branch.read_cap.as_ref().unwrap().key,
222            &publisher_pubkey,
223        );
224        let commit_key = commit.key().unwrap();
225        let mut encrypted_commit_key = Vec::from(commit_key.slice());
226        let mut nonce = seq.to_le_bytes().to_vec();
227        nonce.append(&mut vec![0; 4]);
228        let mut cipher = ChaCha20::new((&key).into(), (nonce.as_slice()).into());
229        cipher.apply_keystream(encrypted_commit_key.as_mut_slice());
230
231        let mut blocks = vec![];
232        for bid in commit.blocks().iter() {
233            blocks.push(store.get(bid)?);
234        }
235        for bid in additional_blocks.iter() {
236            blocks.push(store.get(bid)?);
237        }
238        let event_content = EventContentV0 {
239            topic: *topic_id,
240            publisher: PeerId::Forwarded(publisher_pubkey),
241            seq,
242            blocks,
243            file_ids: commit
244                .header()
245                .as_ref()
246                .map_or_else(|| vec![], |h| h.files().to_vec()),
247            key: encrypted_commit_key,
248        };
249        let event_content_ser = serde_bare::to_vec(&event_content).unwrap();
250        let topic_sig = sign(topic_priv_key, topic_id, &event_content_ser)?;
251        let peer_sig = sign(publisher, &publisher_pubkey, &event_content_ser)?;
252        Ok(EventV0 {
253            content: event_content,
254            topic_sig,
255            peer_sig,
256        })
257    }
258
259    // pub fn put_blocks<'a>(
260    //     &self,
261    //     overlay: &OverlayId,
262    //     storage: &RwLockWriteGuard<'a, dyn BlockStorage + Send + Sync + 'static>,
263    // ) -> Result<ObjectId, NgError> {
264    //     let mut first_id = None;
265    //     for block in &self.content.blocks {
266    //         let id = storage.put(overlay, block)?;
267    //         if first_id.is_none() {
268    //             first_id = Some(id)
269    //         }
270    //     }
271    //     first_id.ok_or(NgError::CommitLoadError(CommitLoadError::NotACommit))
272    // }
273
274    /// opens an event with the key derived from information kept in Repo.
275    ///
276    /// returns the Commit object and optional list of additional block IDs.
277    /// Those blocks have been added to the storage of store of repo so they can be retrieved.
278    pub fn open_with_info(&self, repo: &Repo, branch: &BranchInfo) -> Result<Commit, NgError> {
279        self.open(
280            &repo.store,
281            &repo.id,
282            &branch.id,
283            &branch.read_cap.as_ref().unwrap().key,
284            true,
285        )
286    }
287
288    pub fn open(
289        &self,
290        store: &Store,
291        repo_id: &RepoId,
292        branch_id: &BranchId,
293        branch_secret: &ReadCapSecret,
294        with_body: bool,
295    ) -> Result<Commit, NgError> {
296        // verifying event signatures
297        self.verify()?;
298
299        let publisher_pubkey = self.content.publisher.get_pub_key();
300        let key = Self::derive_key(repo_id, branch_id, branch_secret, &publisher_pubkey);
301        let mut encrypted_commit_key = self.content.key.clone();
302        let mut nonce = self.content.seq.to_le_bytes().to_vec();
303        nonce.append(&mut vec![0; 4]);
304        let mut cipher = ChaCha20::new((&key).into(), (nonce.as_slice()).into());
305        cipher.apply_keystream(encrypted_commit_key.as_mut_slice());
306
307        let commit_key: SymKey = encrypted_commit_key.as_slice().try_into()?;
308
309        let mut first_id = None;
310        for block in &self.content.blocks {
311            let id = store.put(block)?;
312            if first_id.is_none() {
313                first_id = Some(id)
314            }
315        }
316        let commit_ref = ObjectRef::from_id_key(first_id.unwrap(), commit_key);
317        Ok(Commit::load(commit_ref, &store, with_body)?)
318    }
319}