1use super::{AccessMode, AccessSecrets, DecodeError};
2use crate::protocol::RepositoryId;
3use base64::{engine::general_purpose::URL_SAFE_NO_PAD as BASE64, Engine};
4use bincode::Options;
5use ouisync_macros::api;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::{
8 fmt,
9 str::{self, FromStr},
10};
11use zeroize::Zeroizing;
12
13pub const PREFIX: &str = "https://ouisync.net/r";
14pub const VERSION: u64 = 1;
15
16#[derive(Clone, Eq, PartialEq, Debug)]
19#[api(repr(String))]
20pub struct ShareToken {
21 secrets: AccessSecrets,
22 name: String,
23}
24
25impl ShareToken {
26 pub fn with_name(self, name: impl Into<String>) -> Self {
28 Self {
29 name: name.into(),
30 ..self
31 }
32 }
33
34 pub fn id(&self) -> &RepositoryId {
36 self.secrets.id()
37 }
38
39 pub fn suggested_name(&self) -> &str {
41 &self.name
42 }
43
44 pub fn secrets(&self) -> &AccessSecrets {
45 &self.secrets
46 }
47
48 pub fn into_secrets(self) -> AccessSecrets {
49 self.secrets
50 }
51
52 pub fn access_mode(&self) -> AccessMode {
53 self.secrets.access_mode()
54 }
55}
56
57impl From<AccessSecrets> for ShareToken {
58 fn from(secrets: AccessSecrets) -> Self {
59 Self {
60 secrets,
61 name: String::new(),
62 }
63 }
64}
65
66impl FromStr for ShareToken {
67 type Err = DecodeError;
68
69 fn from_str(input: &str) -> Result<Self, Self::Err> {
70 let input = input.trim();
73 let input = input.strip_prefix(PREFIX).ok_or(DecodeError)?;
74
75 let input = match input.strip_prefix('/') {
77 Some(input) => input,
78 None => input,
79 };
80
81 let input = input.strip_prefix('#').ok_or(DecodeError)?;
82
83 let (input, params) = input.split_once('?').unwrap_or((input, ""));
84
85 let input = Zeroizing::new(BASE64.decode(input)?);
86 let input = decode_version(&input)?;
87
88 let secrets: AccessSecrets = bincode::options().deserialize(input)?;
89 let name = parse_name(params)?;
90
91 Ok(Self::from(secrets).with_name(name))
92 }
93}
94
95fn parse_name(query: &str) -> Result<String, DecodeError> {
96 let value = query
97 .split('&')
98 .find_map(|param| param.strip_prefix("name="))
99 .unwrap_or("");
100
101 Ok(urlencoding::decode(value)?.into_owned())
102}
103
104fn encode_version(output: &mut Vec<u8>, version: u64) {
105 let version = vint64::encode(version);
106 output.extend_from_slice(version.as_ref());
107}
108
109fn decode_version(mut input: &[u8]) -> Result<&[u8], DecodeError> {
110 let version = vint64::decode(&mut input).map_err(|_| DecodeError)?;
111 if version == VERSION {
112 Ok(input)
113 } else {
114 Err(DecodeError)
115 }
116}
117
118impl fmt::Display for ShareToken {
119 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120 write!(f, "{}#", PREFIX)?;
121
122 let mut buffer = Vec::new();
123 encode_version(&mut buffer, VERSION);
124 bincode::options()
125 .serialize_into(&mut buffer, &self.secrets)
126 .map_err(|_| fmt::Error)?;
127
128 write!(f, "{}", BASE64.encode(buffer))?;
129
130 if !self.name.is_empty() {
131 write!(f, "?name={}", urlencoding::encode(&self.name))?
132 }
133
134 Ok(())
135 }
136}
137
138impl Serialize for ShareToken {
139 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
140 where
141 S: Serializer,
142 {
143 self.to_string().serialize(s)
144 }
145}
146
147impl<'de> Deserialize<'de> for ShareToken {
148 fn deserialize<D>(d: D) -> Result<Self, D::Error>
149 where
150 D: Deserializer<'de>,
151 {
152 let s = <&str>::deserialize(d)?;
153 let v = s.parse().map_err(serde::de::Error::custom)?;
154 Ok(v)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::{super::WriteSecrets, *};
161 use crate::crypto::{cipher, sign};
162 use assert_matches::assert_matches;
163
164 #[test]
165 fn to_string_from_string_blind() {
166 let token_id = RepositoryId::random();
167 let token = ShareToken::from(AccessSecrets::Blind { id: token_id });
168
169 let encoded = token.to_string();
170 let decoded: ShareToken = encoded.parse().unwrap();
171
172 assert_eq!(decoded.name, "");
173 assert_matches!(decoded.secrets, AccessSecrets::Blind { id } => {
174 assert_eq!(id, token_id)
175 });
176 }
177
178 #[test]
179 fn to_string_from_string_blind_with_name() {
180 let token_id = RepositoryId::random();
181 let token = ShareToken::from(AccessSecrets::Blind { id: token_id }).with_name("foo");
182
183 let encoded = token.to_string();
184 let decoded: ShareToken = encoded.parse().unwrap();
185
186 assert_eq!(decoded.name, token.name);
187 assert_matches!(decoded.secrets, AccessSecrets::Blind { id } => assert_eq!(id, token_id));
188 }
189
190 #[test]
191 fn to_string_from_string_reader() {
192 let token_id = RepositoryId::random();
193 let token_read_key = cipher::SecretKey::random();
194 let token = ShareToken::from(AccessSecrets::Read {
195 id: token_id,
196 read_key: token_read_key.clone(),
197 })
198 .with_name("foo");
199
200 let encoded = token.to_string();
201 let decoded: ShareToken = encoded.parse().unwrap();
202
203 assert_eq!(decoded.name, token.name);
204 assert_matches!(decoded.secrets, AccessSecrets::Read { id, read_key } => {
205 assert_eq!(id, token_id);
206 assert_eq!(read_key.as_ref(), token_read_key.as_ref());
207 });
208 }
209
210 #[test]
211 fn to_string_from_string_writer() {
212 let token_write_keys = sign::Keypair::random();
213 let token_id = RepositoryId::from(token_write_keys.public_key());
214
215 let token =
216 ShareToken::from(AccessSecrets::Write(token_write_keys.into())).with_name("foo");
217
218 let encoded = token.to_string();
219 let decoded: ShareToken = encoded.parse().unwrap();
220
221 assert_eq!(decoded.name, token.name);
222 assert_matches!(decoded.secrets, AccessSecrets::Write(access) => {
223 assert_eq!(access.id, token_id);
224 });
225 }
226
227 #[test]
228 fn snapshot() {
229 let test_vectors = [
230 (
231 "7f6f2ccdb23f2abb7b69278e947c01c6160a31cf02c19d06d0f6e5ab1d768b95",
232 "https://ouisync.net/r#AwIgf28szbI_Krt7aSeOlHwBxhYKMc8CwZ0G0Pblqx12i5U",
233 "https://ouisync.net/r#AwEg7hqkmkRZ3-gTo89uuIIEEjDHslWEad6B-Hyb8jvxCgMgaaT2LXiRw4hdhJFw84_9uSUJ4ztutD9NnENPVnVU1rY",
234 "https://ouisync.net/r#AwAg7hqkmkRZ3-gTo89uuIIEEjDHslWEad6B-Hyb8jvxCgM",
235 ),
236 (
237 "117be1de549d1d4322c4711f11efa0c5137903124f85fc37c761ffc91ace30cb",
238 "https://ouisync.net/r#AwIgEXvh3lSdHUMixHEfEe-gxRN5AxJPhfw3x2H_yRrOMMs",
239 "https://ouisync.net/r#AwEgDCKeJ6jGnnr-hpAN-86rbvbSB1gsEnoNmf6bP7WiBoogs3QJBS11slqiANc0z-ep6TGMokjJxkxzb-tPahjEjFg",
240 "https://ouisync.net/r#AwAgDCKeJ6jGnnr-hpAN-86rbvbSB1gsEnoNmf6bP7WiBoo",
241 ),
242 (
243 "ac7f0d9eaea4d4bf5438b887e34d0cf87e7f98d97da70eff001850487b2cae23",
244 "https://ouisync.net/r#AwIgrH8Nnq6k1L9UOLiH400M-H5_mNl9pw7_ABhQSHssriM",
245 "https://ouisync.net/r#AwEgKmhZmO5ElTo-sKXTFpN_gQqAvcyVLAqge02Cs_7UWcIgZaQ5F6Mkfsakp1H_AZvkilY9u25vetBkxbVwcdiXkhY",
246 "https://ouisync.net/r#AwAgKmhZmO5ElTo-sKXTFpN_gQqAvcyVLAqge02Cs_7UWcI",
247 ),
248 (
249 "bbb7d40b7bb8e41c550696fdef78fff6f013bb34627ba50ca2d63b6e84cffa6c",
250 "https://ouisync.net/r#AwIgu7fUC3u45BxVBpb973j_9vATuzRie6UMotY7boTP-mw",
251 "https://ouisync.net/r#AwEgf3_dHKjXw-2CBhNxeLR7yv56VNSgtM5b2eJZeBhLSM4g4l_rARpMZdhkhudmQlO4xgvJC9kf7FAhAy-7XmARe6Q",
252 "https://ouisync.net/r#AwAgf3_dHKjXw-2CBhNxeLR7yv56VNSgtM5b2eJZeBhLSM4",
253 ),
254 (
255 "9a32e1a6638ce87528a3f0303c7a9cecba4ed5fef0551f3afd1c7865bc66308f",
256 "https://ouisync.net/r#AwIgmjLhpmOM6HUoo_AwPHqc7LpO1f7wVR86_Rx4ZbxmMI8",
257 "https://ouisync.net/r#AwEgPzwkeuagmeh1RyeOg9K1Gta4-4X_HXJl6EnPeCqHHXAgigK_99pB7Mqq75MYmxnffBzudb0swtGDZXG7IfTe0Xk",
258 "https://ouisync.net/r#AwAgPzwkeuagmeh1RyeOg9K1Gta4-4X_HXJl6EnPeCqHHXA",
259 ),
260 (
261 "eb6a97af1f95c72764a092b8794ce3d5b14fef7697095f34f33e5f13a814cd80",
262 "https://ouisync.net/r#AwIg62qXrx-VxydkoJK4eUzj1bFP73aXCV808z5fE6gUzYA",
263 "https://ouisync.net/r#AwEgQPSvtDyRS7cw7gUOo6MY6Yy7Si5n3vXQ6H1yqkULlzggnNrwTb8H3OrkTNEt2E1LjjdUGSvCqqxknmtbaCJUTGU",
264 "https://ouisync.net/r#AwAgQPSvtDyRS7cw7gUOo6MY6Yy7Si5n3vXQ6H1yqkULlzg",
265 ),
266 (
267 "8d4e5ee1d08b43a3d80457bf09e0957a2f922b58e79646e02a2529cb7c99e3de",
268 "https://ouisync.net/r#AwIgjU5e4dCLQ6PYBFe_CeCVei-SK1jnlkbgKiUpy3yZ494",
269 "https://ouisync.net/r#AwEgVtEZXw7riIBZBZBedrmi3XfzuevmO1No3sbyIv9TomsgEttex_j8LvPYeT6CesaXqTh9XY1JViWmp0FGblnUAOM",
270 "https://ouisync.net/r#AwAgVtEZXw7riIBZBZBedrmi3XfzuevmO1No3sbyIv9Toms",
271 ),
272 (
273 "560162bb28f02f1015a3dcec38dca4fc73535b298b0b8037077edc6fe22b20fa",
274 "https://ouisync.net/r#AwIgVgFiuyjwLxAVo9zsONyk_HNTWymLC4A3B37cb-IrIPo",
275 "https://ouisync.net/r#AwEg5SzdGSsWn9u9YLqjK6991V6Yuh_chZAR27P0CIpAHsggY-m-q0MtHuQ1h9zpJNXpEQOnOiC5nEfPquVyGEqHTKA",
276 "https://ouisync.net/r#AwAg5SzdGSsWn9u9YLqjK6991V6Yuh_chZAR27P0CIpAHsg",
277 ),
278 (
279 "72cc0b4cee98ddeaa5a0626311355dad94690e6110aed80397ea92d13a82b811",
280 "https://ouisync.net/r#AwIgcswLTO6Y3eqloGJjETVdrZRpDmEQrtgDl-qS0TqCuBE",
281 "https://ouisync.net/r#AwEg0qIB6AJ497v8pBn15tTP19jYkxp9ePRSM73lR51ObVwgIynW_5WAjN3jkOy9KgIxCMzDInogwcpVE9JhyY-1fIg",
282 "https://ouisync.net/r#AwAg0qIB6AJ497v8pBn15tTP19jYkxp9ePRSM73lR51ObVw",
283 ),
284 (
285 "e3fd1ddb28613f9ead9869b392fa1f9d91bef1ab625605c968c72f5312ac77aa",
286 "https://ouisync.net/r#AwIg4_0d2yhhP56tmGmzkvofnZG-8atiVgXJaMcvUxKsd6o",
287 "https://ouisync.net/r#AwEg824LDR5VNan2dNT220rBhdojmoD6BxnZ-olCblcMfPcgtGlE43E57F6gLJz3dee8c-8rH0JBpiZ1np-YbGYtW0k",
288 "https://ouisync.net/r#AwAg824LDR5VNan2dNT220rBhdojmoD6BxnZ-olCblcMfPc",
289 ),
290 (
291 "8e8958dddba89b1547d9efd010b37e156ebc6bf41a4dca84d67c6bdca88ac0c0",
292 "https://ouisync.net/r#AwIgjolY3duomxVH2e_QELN-FW68a_QaTcqE1nxr3KiKwMA",
293 "https://ouisync.net/r#AwEgsSu6XyhPQ82lz7XsBN4N4VxDW1JQD3WzQleLsxVPqjggCrXQ5tAbYgwX_0SXtY81S3zIY2Ta6bsIG_t7ySzj2qM",
294 "https://ouisync.net/r#AwAgsSu6XyhPQ82lz7XsBN4N4VxDW1JQD3WzQleLsxVPqjg",
295 ),
296 (
297 "c3420141d57426de45356f2a84456d169bc4593c8a23e359b898dbe51b4ef62f",
298 "https://ouisync.net/r#AwIgw0IBQdV0Jt5FNW8qhEVtFpvEWTyKI-NZuJjb5RtO9i8",
299 "https://ouisync.net/r#AwEgwqmZu-rGbq9Ph2YbCnlWY37fGbri2BHJP43m7dLqgxwgJjbsCKHZG3k7DfrUYNdjnKfL588jkstlJ1mg5-xMehM",
300 "https://ouisync.net/r#AwAgwqmZu-rGbq9Ph2YbCnlWY37fGbri2BHJP43m7dLqgxw",
301 ),
302 (
303 "10eb99e218c5be855df2859f267d07fa024a19693b96894336ac426710210a40",
304 "https://ouisync.net/r#AwIgEOuZ4hjFvoVd8oWfJn0H-gJKGWk7lolDNqxCZxAhCkA",
305 "https://ouisync.net/r#AwEgUgZT1ofDl1Vo6V1OqxBNTlsEpATy8aYgk3E0iFFIMe8gaqaBmsR0qk8h1zBKWsinZk2IjkJtKNSa6to-bbmoavk",
306 "https://ouisync.net/r#AwAgUgZT1ofDl1Vo6V1OqxBNTlsEpATy8aYgk3E0iFFIMe8",
307 ),
308 (
309 "ceadb07bb79926fcd95ad7fff5d37faa7dd2df1849940cacd208a79fb3a0b2ca",
310 "https://ouisync.net/r#AwIgzq2we7eZJvzZWtf_9dN_qn3S3xhJlAys0ginn7Ogsso",
311 "https://ouisync.net/r#AwEgkbc-pQLI2oVLZBF1vUKwCCzqyryexuRJZTfrjLHdG1gg0t9r4gtj7SZuBi63NtCDd-y3nTUp6VkvtErsHgs3af8",
312 "https://ouisync.net/r#AwAgkbc-pQLI2oVLZBF1vUKwCCzqyryexuRJZTfrjLHdG1g",
313 ),
314 (
315 "87cb50b2635ce54aa15782cd2b9c6ca22b2501eabdf3c808fa1ab8ea93176615",
316 "https://ouisync.net/r#AwIgh8tQsmNc5UqhV4LNK5xsoislAeq988gI-hq46pMXZhU",
317 "https://ouisync.net/r#AwEgXw0gkijsdg0BggUw3N89-tQFzIp3a8nAOp4Gq8z2a3QgJI7N12DueZ4ev7WiJiWUDc20Ugffci3PqG6MJxey0WE",
318 "https://ouisync.net/r#AwAgXw0gkijsdg0BggUw3N89-tQFzIp3a8nAOp4Gq8z2a3Q",
319 ),
320 (
321 "27e0fb035ae58c31ffa84dbf181ce7a72ab61bbefb8bda94950b7c0fdcbacf2f",
322 "https://ouisync.net/r#AwIgJ-D7A1rljDH_qE2_GBznpyq2G777i9qUlQt8D9y6zy8",
323 "https://ouisync.net/r#AwEgqTfkcLMFVLpFuwxEi9dhWtBKDBfkLVgUI0LohEAQ2ycgc-EyPvCft0S-gRCim6DTMjWhJnxvOuuYWyid0Uee6-A",
324 "https://ouisync.net/r#AwAgqTfkcLMFVLpFuwxEi9dhWtBKDBfkLVgUI0LohEAQ2yc",
325 ),
326 ];
327
328 for (secret_key_hex, expected_write, expected_read, expected_blind) in test_vectors {
329 let mut secret_key = [0; sign::Keypair::SECRET_KEY_SIZE];
330 hex::decode_to_slice(secret_key_hex, &mut secret_key).unwrap();
331
332 let secrets =
333 AccessSecrets::Write(WriteSecrets::from(sign::Keypair::from(&secret_key)));
334
335 assert_eq!(
336 ShareToken::from(secrets.clone()).to_string(),
337 expected_write,
338 );
339
340 assert_eq!(
341 ShareToken::from(secrets.with_mode(AccessMode::Read)).to_string(),
342 expected_read,
343 );
344
345 assert_eq!(
346 ShareToken::from(secrets.with_mode(AccessMode::Blind)).to_string(),
347 expected_blind,
348 );
349 }
350 }
351}