ouisync/access_control/
share_token.rs

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/// Token to share a repository. It can be encoded as a URL-formatted string and transmitted to
17/// other replicas.
18#[derive(Clone, Eq, PartialEq, Debug)]
19#[api(repr(String))]
20pub struct ShareToken {
21    secrets: AccessSecrets,
22    name: String,
23}
24
25impl ShareToken {
26    /// Attach a suggested repository name to the token.
27    pub fn with_name(self, name: impl Into<String>) -> Self {
28        Self {
29            name: name.into(),
30            ..self
31        }
32    }
33
34    /// Id of the repository to share.
35    pub fn id(&self) -> &RepositoryId {
36        self.secrets.id()
37    }
38
39    /// Suggested name of the repository, if provided.
40    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        // Trim from the end as well because reading lines from a file includes the `\n` character.
71        // Also the user may accidentally include white space if done from the app.
72        let input = input.trim();
73        let input = input.strip_prefix(PREFIX).ok_or(DecodeError)?;
74
75        // The '/' before '#...' is optional.
76        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}