1use std::str::FromStr;
2
3use crate::operation::{SignedOperation, SignedPLCOperation};
4use crate::{operation::PLCOperationType, util::op_from_json};
5use crate::{PLCError, PLCOperation};
6use chrono::{DateTime, Local, NaiveDateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize, Clone)]
10#[serde(rename_all = "camelCase")]
11pub struct AuditLog {
12 pub cid: String,
13 pub created_at: NaiveDateTime,
14 pub did: String,
15 pub nullified: bool,
16 pub operation: PLCOperation,
17}
18
19impl AuditLog {
20 pub fn from_json(json: &str) -> Result<Self, PLCError> {
21 let json: serde_json::Value =
22 serde_json::from_str(json).map_err(|e| PLCError::Other(e.into()))?;
23 let op = op_from_json(
24 &serde_json::to_string(json.get("operation").unwrap())
25 .map_err(|e| PLCError::Other(e.into()))?,
26 )?;
27 Ok(AuditLog {
28 cid: json.get("cid").unwrap().as_str().unwrap().to_string(),
29 created_at: DateTime::<Utc>::from_str(json.get("createdAt").unwrap().as_str().unwrap())
30 .unwrap()
31 .naive_utc(),
32 did: json.get("did").unwrap().as_str().unwrap().to_string(),
33 nullified: json.get("nullified").unwrap().as_bool().unwrap(),
34 operation: op,
35 })
36 }
37
38 pub fn op_type(&self) -> PLCOperationType {
39 match &self.operation {
40 PLCOperation::SignedGenesis(_) => PLCOperationType::Operation,
41 PLCOperation::UnsignedGenesis(_) => PLCOperationType::Operation,
42 PLCOperation::SignedPLC(op) => op.unsigned.type_.clone(),
43 PLCOperation::UnsignedPLC(op) => op.type_.clone(),
44 }
45 }
46}
47
48#[derive(Serialize, Deserialize, Clone)]
49pub struct DIDAuditLogs(Vec<AuditLog>);
50
51impl DIDAuditLogs {
52 pub fn from_json(json: &str) -> Result<Self, PLCError> {
53 let json: serde_json::Value =
54 serde_json::from_str(json).map_err(|e| PLCError::Other(e.into()))?;
55 let logs = json
56 .as_array()
57 .unwrap()
58 .iter()
59 .map(|log| AuditLog::from_json(&serde_json::to_string(log).unwrap()).unwrap())
60 .collect();
61 Ok(Self(logs))
62 }
63
64 pub fn get_latest(&self) -> Result<String, PLCError> {
65 let last_op = self.0.last();
66 if last_op.is_none() {
67 return Err(PLCError::MisorderedOperation);
68 }
69 let mut last_op = last_op.unwrap();
70 if last_op.op_type() == PLCOperationType::Tombstone {
71 if self.0.len() == 1 {
72 return Err(PLCError::InvalidOperation);
73 }
74 last_op = &self.0[self.0.len() - 2];
75 }
76 Ok(last_op.cid.clone())
77 }
78
79 pub fn assure_valid(&self, proposed: SignedPLCOperation) -> Result<bool, PLCError> {
80 let cid = match &proposed.unsigned.prev {
81 Some(cid) => cid,
82 None => return Err(PLCError::MisorderedOperation),
83 };
84 let index_of_prev = self.0.iter().position(|log| log.cid == cid.to_string());
85
86 if index_of_prev.is_none() {
87 return Err(PLCError::MisorderedOperation);
88 }
89
90 let ops_in_history = self.0[0..index_of_prev.unwrap()].to_vec();
91 let nullified = self.0[index_of_prev.unwrap()..self.0.len() - 1].to_vec();
92
93 let last_op = ops_in_history.last();
94 if last_op.is_none() {
95 return Err(PLCError::MisorderedOperation);
96 }
97 let last_op = last_op.unwrap();
98 if last_op.op_type() == PLCOperationType::Tombstone {
99 return Err(PLCError::MisorderedOperation);
100 }
101
102 let last_op_normalized: SignedPLCOperation = match &last_op.operation {
103 PLCOperation::SignedGenesis(op) => op.normalize().unwrap().into(),
104 PLCOperation::SignedPLC(op) => op.clone(),
105 _ => {
106 unreachable!()
107 }
108 };
109
110 let first_nullified = nullified.first();
112 if first_nullified.is_none() {
113 match last_op_normalized.verify_sig(None) {
114 Ok(_) => {
115 return Ok(true);
116 }
117 Err(_) => {
118 return Err(PLCError::InvalidSignature);
119 }
120 }
121 }
122 let first_nullified = first_nullified.unwrap();
123
124 let (_, disputed_key) = match last_op_normalized.verify_sig(None) {
125 Ok(result) => {
126 if !result.0 {
127 return Err(PLCError::InvalidSignature);
128 }
129 result
130 }
131 Err(_) => {
132 return Err(PLCError::InvalidSignature);
133 }
134 };
135 let disputed_key = disputed_key.unwrap();
136
137 let signer_index = last_op_normalized
138 .unsigned
139 .rotation_keys
140 .iter()
141 .position(|key| key == &disputed_key)
142 .unwrap();
143 let more_powerful_keys = last_op_normalized
144 .unsigned
145 .rotation_keys
146 .split_at(signer_index)
147 .1
148 .to_vec();
149
150 match proposed.verify_sig(Some(more_powerful_keys)) {
151 Ok(result) => {
152 if !result.0 {
153 return Err(PLCError::InvalidSignature);
154 }
155 }
156 Err(_) => {
157 return Err(PLCError::InvalidSignature);
158 }
159 }
160
161 if nullified.len() > 0 {
162 const RECOVERY_WINDOW: i64 = 72 * 60 * 60;
163 let local = Local::now().naive_utc();
164 let time_lapsed = local - first_nullified.created_at;
165 if time_lapsed.num_seconds() > RECOVERY_WINDOW {
166 return Err(PLCError::LateRecovery);
167 }
168 }
169
170 return Ok(true);
171 }
172
173 pub fn last(&self) -> Option<&AuditLog> {
174 self.0.last()
175 }
176
177 pub fn len(&self) -> usize {
178 self.0.len()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 const TEST_AUDIT_LOG: &str = "[{\"did\":\"did:plc:z72i7hdynmk6r22z27h6tvur\",\"operation\":{\"sig\":\"9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA\",\"prev\":null,\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://bsky.social\"}},\"alsoKnownAs\":[\"at://bluesky-team.bsky.social\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"}},\"cid\":\"bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm\",\"nullified\":false,\"createdAt\":\"2023-04-12T04:53:57.057Z\"},{\"did\":\"did:plc:z72i7hdynmk6r22z27h6tvur\",\"operation\":{\"sig\":\"1mEWzRtFOgeRXH-YCSPTxb990JOXxa__n8Qw6BOKl7Ndm6OFFmwYKiiMqMCpAbxpnGjF5abfIsKc7u3a77Cbnw\",\"prev\":\"bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm\",\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://bsky.social\"}},\"alsoKnownAs\":[\"at://bsky.app\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"}},\"cid\":\"bafyreihmuvr3frdvd6vmdhucih277prdcfcezf67lasg5oekxoimnunjoq\",\"nullified\":false,\"createdAt\":\"2023-04-12T17:26:46.468Z\"},{\"did\":\"did:plc:z72i7hdynmk6r22z27h6tvur\",\"operation\":{\"sig\":\"OoDJihYhLUEWp2MGiAoCN1sRj9cgUEqNjZe6FIOePB8Ugp-IWAZplFRm-pU-fbYSpYV1_tQ9Gx8d_PR9f3NBAg\",\"prev\":\"bafyreihmuvr3frdvd6vmdhucih277prdcfcezf67lasg5oekxoimnunjoq\",\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://bsky.social\"}},\"alsoKnownAs\":[\"at://bsky.app\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"}},\"cid\":\"bafyreiexwziulimyiw3qlhpwr2zljk5jtzdp2bgqbgoxuemjsf5a6tan3a\",\"nullified\":false,\"createdAt\":\"2023-06-01T20:05:52.008Z\"},{\"did\":\"did:plc:z72i7hdynmk6r22z27h6tvur\",\"operation\":{\"sig\":\"8Wj9Cf74dZFNKx7oucZSHbBDFOMJ3xx9lkvj5rT9xMErssWYl1D9n4PeGC0mNml7xDG7uoQqZ1JWoApGADUgXg\",\"prev\":\"bafyreiexwziulimyiw3qlhpwr2zljk5jtzdp2bgqbgoxuemjsf5a6tan3a\",\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://puffball.us-east.host.bsky.network\"}},\"alsoKnownAs\":[\"at://bsky.app\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shQo6TF2moaqMTrUZEM1jeuYRQXeHEx4evX9751y2qPqRA\"}},\"cid\":\"bafyreifn4pkect7nymne3sxkdg7tn7534msyxcjkshmzqtijmn3enyxm3q\",\"nullified\":false,\"createdAt\":\"2023-11-09T21:49:10.793Z\"}]";
187
188 #[test]
189 fn test_did_audit_log_from_json() {
190 let audit_logs = DIDAuditLogs::from_json(TEST_AUDIT_LOG).unwrap();
191 assert_eq!(audit_logs.0.len(), 4);
192 assert_eq!(audit_logs.last().unwrap().cid, "bafyreifn4pkect7nymne3sxkdg7tn7534msyxcjkshmzqtijmn3enyxm3q");
193
194 match &audit_logs.last().unwrap().operation {
195 PLCOperation::SignedPLC(op) => {
196 assert_eq!(op.unsigned.type_, PLCOperationType::Operation);
197 }
198 _ => {
199 unreachable!()
200 }
201 }
202
203 assert_eq!(audit_logs.last().unwrap().nullified, false);
204 assert_eq!(audit_logs.last().unwrap().created_at, NaiveDateTime::parse_from_str("2023-11-09T21:49:10.793Z", "%Y-%m-%dT%H:%M:%S%.fZ").unwrap());
205 }
206}