1use crate::types::Label;
7
8pub struct LabelSigner {
13 key: Vec<u8>,
15}
16
17impl LabelSigner {
18 pub fn new(key: Vec<u8>) -> Self {
20 Self { key }
21 }
22
23 pub fn from_base64(encoded: &str) -> anyhow::Result<Self> {
28 Ok(Self {
31 key: encoded.as_bytes().to_vec(),
32 })
33 }
34
35 pub fn sign(&self, label: &Label) -> anyhow::Result<String> {
45 let label_json = serde_json::to_vec(label)?;
49
50 let mut hasher_input = Vec::with_capacity(self.key.len() + label_json.len());
51 hasher_input.extend_from_slice(&self.key);
52 hasher_input.extend_from_slice(&label_json);
53
54 let hash = simple_hash(&hasher_input);
55 Ok(base64_encode(&hash))
56 }
57}
58
59fn simple_hash(data: &[u8]) -> Vec<u8> {
63 let mut hash: u64 = 5381;
64 for &byte in data {
65 hash = hash.wrapping_mul(33).wrapping_add(byte as u64);
66 }
67 hash.to_be_bytes().to_vec()
68}
69
70fn base64_encode(data: &[u8]) -> String {
72 const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
73 let mut result = String::new();
74 for chunk in data.chunks(3) {
75 let b0 = chunk[0] as u32;
76 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
77 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
78 let triple = (b0 << 16) | (b1 << 8) | b2;
79
80 result.push(CHARSET[((triple >> 18) & 0x3F) as usize] as char);
81 result.push(CHARSET[((triple >> 12) & 0x3F) as usize] as char);
82 if chunk.len() > 1 {
83 result.push(CHARSET[((triple >> 6) & 0x3F) as usize] as char);
84 } else {
85 result.push('=');
86 }
87 if chunk.len() > 2 {
88 result.push(CHARSET[(triple & 0x3F) as usize] as char);
89 } else {
90 result.push('=');
91 }
92 }
93 result
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::types::Label;
100
101 #[test]
102 fn sign_produces_non_empty_string() {
103 let signer = LabelSigner::new(b"test-key".to_vec());
104 let label = Label {
105 ver: 1,
106 src: "did:plc:test".to_string(),
107 uri: "at://did:plc:user/app.bsky.feed.post/abc".to_string(),
108 cid: None,
109 val: "spam".to_string(),
110 neg: false,
111 cts: "1970-01-01T00:00:00Z".to_string(),
112 exp: None,
113 };
114
115 let sig = signer.sign(&label).unwrap();
116 assert!(!sig.is_empty());
117 }
118
119 #[test]
120 fn sign_is_deterministic() {
121 let signer = LabelSigner::new(b"key".to_vec());
122 let label = Label {
123 ver: 1,
124 src: "did:plc:test".to_string(),
125 uri: "at://did:plc:user/app.bsky.feed.post/abc".to_string(),
126 cid: None,
127 val: "spam".to_string(),
128 neg: false,
129 cts: "2024-01-01T00:00:00Z".to_string(),
130 exp: None,
131 };
132
133 let sig1 = signer.sign(&label).unwrap();
134 let sig2 = signer.sign(&label).unwrap();
135 assert_eq!(sig1, sig2);
136 }
137
138 #[test]
139 fn base64_encode_empty() {
140 assert_eq!(base64_encode(&[]), "");
141 }
142
143 #[test]
144 fn base64_encode_single_byte() {
145 let result = base64_encode(&[0x4D]);
146 assert_eq!(result, "TQ==");
147 }
148
149 #[test]
150 fn base64_encode_two_bytes() {
151 let result = base64_encode(&[0x4D, 0x61]);
152 assert_eq!(result, "TWE=");
153 }
154
155 #[test]
156 fn base64_encode_three_bytes() {
157 let result = base64_encode(&[0x4D, 0x61, 0x6E]);
158 assert_eq!(result, "TWFu");
159 }
160
161 #[test]
162 fn from_base64_creates_signer() {
163 let signer = LabelSigner::from_base64("dGVzdC1rZXk=").unwrap();
164 let label = Label {
165 ver: 1,
166 src: "did:plc:test".to_string(),
167 uri: "at://did:plc:user/app.bsky.feed.post/abc".to_string(),
168 cid: None,
169 val: "spam".to_string(),
170 neg: false,
171 cts: "2024-01-01T00:00:00Z".to_string(),
172 exp: None,
173 };
174 let sig = signer.sign(&label).unwrap();
175 assert!(!sig.is_empty());
176 }
177}