├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── main.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── roundtrip_box.rs │ ├── roundtrip_feed.rs │ ├── roundtrip_hash.rs │ └── roundtrip_key.rs └── src ├── lib.rs ├── multibox.rs ├── multifeed.rs ├── multihash.rs └── multikey.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ssb-multiformats" 3 | version = "0.4.2" 4 | authors = ["AljoschaMeyer "] 5 | edition = "2018" 6 | license = "LGPL-3.0" 7 | description = "Key, feed id, and message hash types used by Secure Scuttlebutt" 8 | repository = "https://github.com/sunrise-choir/ssb-multiformats" 9 | documentation = "https://docs.rs/ssb-multiformats/" 10 | readme = "README.md" 11 | keywords = ["ssb", "scuttlebutt"] 12 | 13 | [dependencies] 14 | base64 = "0.13.0" 15 | serde = "1.0.126" 16 | ssb-crypto = {version = "0.2.3", default_features = false} 17 | 18 | [dev-dependencies] 19 | matches = "0.1.8" 20 | 21 | [features] 22 | default = ["dalek"] 23 | dalek = ["ssb-crypto/default"] 24 | sodium = ["ssb-crypto/sodium", "ssb-crypto/alloc"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSB Multiformats 2 | 3 | Implementation of the [SSB multiformats](https://spec.scuttlebutt.nz/feed/datatypes.html) in Rust. 4 | -------------------------------------------------------------------------------- /examples/main.rs: -------------------------------------------------------------------------------- 1 | extern crate ssb_multiformats; 2 | 3 | use std::fs::{self, File}; 4 | use std::io::{self, prelude::*}; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use ssb_multiformats::{multibox::Multibox, multihash::Multihash, multikey::Multikey}; 8 | 9 | fn main() { 10 | handle_nays_key(Path::new("../multiformats-testdata/multikey/nay")).unwrap(); 11 | handle_yays_key(Path::new("../multiformats-testdata/multikey/yay")).unwrap(); 12 | handle_nays_hash(Path::new("../multiformats-testdata/multihash/nay")).unwrap(); 13 | handle_yays_hash(Path::new("../multiformats-testdata/multihash/yay")).unwrap(); 14 | 15 | let paths = fs::read_dir("./fuzz/corpus/roundtrip_box").unwrap(); 16 | 17 | for dir_path in paths { 18 | let path = dir_path.unwrap().path(); 19 | let mut file = File::open(path.clone()).unwrap(); 20 | let mut contents = Vec::new(); 21 | file.read_to_end(&mut contents).unwrap(); 22 | 23 | match Multibox::from_legacy(&contents) { 24 | Ok(_) => { 25 | let (actual, _tail) = split_at_byte(&contents, 0x22).unwrap(); 26 | 27 | let mut p = PathBuf::from("../multiformats-testdata/multibox/yay"); 28 | p.push(path.file_name().unwrap()); 29 | 30 | let mut file = File::create(&p).unwrap(); 31 | file.write_all(actual).unwrap(); 32 | } 33 | 34 | Err(_) => { 35 | let mut p = PathBuf::from("../multiformats-testdata/multibox/nay"); 36 | p.push(path.file_name().unwrap()); 37 | 38 | let mut file = File::create(&p).unwrap(); 39 | file.write_all(&contents).unwrap(); 40 | } 41 | } 42 | } 43 | } 44 | 45 | fn handle_nays_key(path: &Path) -> Result<(), io::Error> { 46 | let paths = fs::read_dir(path)?; 47 | 48 | let mut i = 0; 49 | 50 | for dir_path in paths { 51 | let path = dir_path.unwrap().path(); 52 | let mut file = File::open(path.clone())?; 53 | let mut contents = Vec::new(); 54 | file.read_to_end(&mut contents)?; 55 | 56 | match Multikey::from_legacy(&contents) { 57 | Ok(e) => { 58 | if e.1.len() == 0 { 59 | println!("{:?}", path); 60 | println!("{:?}", &contents); 61 | println!("{:?}", std::str::from_utf8(&contents)); 62 | println!("{:?}", e); 63 | panic!() 64 | } 65 | } 66 | 67 | Err(_) => {} 68 | } 69 | 70 | i += 1; 71 | } 72 | 73 | println!("handled {} files at {:?}", i, path); 74 | Ok(()) 75 | } 76 | 77 | fn handle_yays_key(path: &Path) -> Result<(), io::Error> { 78 | let paths = fs::read_dir(path)?; 79 | 80 | let mut i = 0; 81 | 82 | for dir_path in paths { 83 | let path = dir_path.unwrap().path(); 84 | let mut file = File::open(path.clone())?; 85 | let mut contents = Vec::new(); 86 | file.read_to_end(&mut contents)?; 87 | 88 | match Multikey::from_legacy(&contents) { 89 | Ok(_) => {} 90 | 91 | Err(e) => { 92 | println!("{:?}", path); 93 | println!("{:?}", &contents); 94 | println!("{:?}", std::str::from_utf8(&contents)); 95 | println!("{:?}", e); 96 | panic!() 97 | } 98 | } 99 | 100 | i += 1; 101 | } 102 | 103 | println!("handled {} files at {:?}", i, path); 104 | Ok(()) 105 | } 106 | 107 | fn handle_nays_hash(path: &Path) -> Result<(), io::Error> { 108 | let paths = fs::read_dir(path)?; 109 | 110 | let mut i = 0; 111 | 112 | for dir_path in paths { 113 | let path = dir_path.unwrap().path(); 114 | let mut file = File::open(path.clone())?; 115 | let mut contents = Vec::new(); 116 | file.read_to_end(&mut contents)?; 117 | 118 | match Multihash::from_legacy(&contents) { 119 | Ok(e) => { 120 | if e.1.len() == 0 { 121 | println!("{:?}", path); 122 | println!("{:?}", &contents); 123 | println!("{:?}", std::str::from_utf8(&contents)); 124 | println!("{:?}", e); 125 | panic!() 126 | } 127 | } 128 | 129 | Err(_) => {} 130 | } 131 | 132 | i += 1; 133 | } 134 | 135 | println!("handled {} files at {:?}", i, path); 136 | Ok(()) 137 | } 138 | 139 | fn handle_yays_hash(path: &Path) -> Result<(), io::Error> { 140 | let paths = fs::read_dir(path)?; 141 | 142 | let mut i = 0; 143 | 144 | for dir_path in paths { 145 | let path = dir_path.unwrap().path(); 146 | let mut file = File::open(path.clone())?; 147 | let mut contents = Vec::new(); 148 | file.read_to_end(&mut contents)?; 149 | 150 | match Multihash::from_legacy(&contents) { 151 | Ok(_) => {} 152 | 153 | Err(e) => { 154 | println!("{:?}", path); 155 | println!("{:?}", &contents); 156 | println!("{:?}", std::str::from_utf8(&contents)); 157 | println!("{:?}", e); 158 | panic!() 159 | } 160 | } 161 | 162 | i += 1; 163 | } 164 | 165 | println!("handled {} files at {:?}", i, path); 166 | Ok(()) 167 | } 168 | 169 | // Split the input slice at the first occurence o the given byte, the byte itself is not 170 | // part of any of the returned slices. Return `None` if the byte is not found in the input. 171 | pub(crate) fn split_at_byte(input: &[u8], byte: u8) -> Option<(&[u8], &[u8])> { 172 | for i in 0..input.len() { 173 | if unsafe { *input.get_unchecked(i) } == byte { 174 | let (start, end) = input.split_at(i); 175 | return Some((start, &end[1..])); 176 | } 177 | } 178 | 179 | return None; 180 | } 181 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "ssb-multiformats-fuzz" 4 | version = "0.0.1" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies.ssb-multiformats] 12 | path = ".." 13 | [dependencies.libfuzzer-sys] 14 | git = "https://github.com/rust-fuzz/libfuzzer-sys.git" 15 | 16 | # Prevent this from interfering with workspaces 17 | [workspace] 18 | members = ["."] 19 | 20 | [[bin]] 21 | name = "roundtrip_key" 22 | path = "fuzz_targets/roundtrip_key.rs" 23 | 24 | [[bin]] 25 | name = "roundtrip_hash" 26 | path = "fuzz_targets/roundtrip_hash.rs" 27 | 28 | [[bin]] 29 | name = "roundtrip_feed" 30 | path = "fuzz_targets/roundtrip_feed.rs" 31 | 32 | [[bin]] 33 | name = "roundtrip_box" 34 | path = "fuzz_targets/roundtrip_box.rs" 35 | 36 | [[bin]] 37 | name = "roundtrip_key_compact" 38 | path = "fuzz_targets/roundtrip_key_compact.rs" 39 | 40 | [[bin]] 41 | name = "roundtrip_hash_compact" 42 | path = "fuzz_targets/roundtrip_hash_compact.rs" 43 | 44 | [[bin]] 45 | name = "roundtrip_feed_compact" 46 | path = "fuzz_targets/roundtrip_feed_compact.rs" 47 | 48 | [[bin]] 49 | name = "roundtrip_box_compact" 50 | path = "fuzz_targets/roundtrip_box_compact.rs" 51 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/roundtrip_box.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate ssb_multiformats; 5 | 6 | use ssb_multiformats::multibox::Multibox; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | // This comment keeps rustfmt from breaking the fuzz macro... 10 | match Multibox::from_legacy(data) { 11 | Ok((k, tail)) => { 12 | let enc = k.to_legacy_string(); 13 | assert_eq!(enc.as_bytes(), &data[..data.len() - (tail.len())]); 14 | } 15 | Err(_) => {} 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/roundtrip_feed.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate ssb_multiformats; 5 | 6 | use ssb_multiformats::multifeed::Multifeed; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | // This comment keeps rustfmt from breaking the fuzz macro... 10 | match Multifeed::from_legacy(data) { 11 | Ok((k, tail)) => { 12 | let enc = k.to_legacy_string(); 13 | assert_eq!(enc.as_bytes(), &data[..data.len() - tail.len()]); 14 | } 15 | Err(_) => {} 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/roundtrip_hash.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate ssb_multiformats; 5 | 6 | use ssb_multiformats::multihash::Multihash; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | // This comment keeps rustfmt from breaking the fuzz macro... 10 | match Multihash::from_legacy(data) { 11 | Ok((k, tail)) => { 12 | let enc = k.to_legacy_string(); 13 | assert_eq!(enc.as_bytes(), &data[..data.len() - tail.len()]); 14 | } 15 | Err(_) => {} 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/roundtrip_key.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate ssb_multiformats; 5 | 6 | use ssb_multiformats::multikey::Multikey; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | // This comment keeps rustfmt from breaking the fuzz macro... 10 | match Multikey::from_legacy(data) { 11 | Ok((k, tail)) => { 12 | let enc = k.to_legacy_string(); 13 | assert_eq!(enc.as_bytes(), &data[..data.len() - tail.len()]); 14 | } 15 | Err(_) => {} 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of the [ssb multiformats](https://spec.scuttlebutt.nz/feed/datatypes.html). 2 | // #![warn(missing_docs)] 3 | 4 | extern crate base64; 5 | extern crate serde; 6 | 7 | #[cfg(test)] 8 | #[macro_use] 9 | extern crate matches; 10 | 11 | pub mod multibox; 12 | pub mod multifeed; 13 | pub mod multihash; 14 | pub mod multikey; 15 | 16 | /////////////////////////////////////////////////////////////////////////////// 17 | // A bunch of helper functions used throughout the crate for parsing legacy encodings. 18 | //////////////////////////////////////////////////////////////////////////////// 19 | 20 | // Split the input slice at the first occurence of the given byte, the byte itself is not 21 | // part of any of the returned slices. Return `None` if the byte is not found in the input. 22 | pub(crate) fn split_at_byte(input: &[u8], byte: u8) -> Option<(&[u8], &[u8])> { 23 | for i in 0..input.len() { 24 | if unsafe { *input.get_unchecked(i) } == byte { 25 | let (start, end) = input.split_at(i); 26 | return Some((start, &end[1..])); 27 | } 28 | } 29 | 30 | None 31 | } 32 | 33 | // If the slice begins with the given prefix, return everything after that prefix. 34 | pub(crate) fn skip_prefix<'a>(input: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> { 35 | if input.starts_with(prefix) { 36 | Some(&input[prefix.len()..]) 37 | } else { 38 | None 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/multibox.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of [ssb multiboxes](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox). 2 | use std::fmt; 3 | use std::io::{self, Write}; 4 | 5 | use super::{base64, skip_prefix, split_at_byte}; 6 | 7 | #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)] 8 | /// A multibox that owns its data. This does no decryption, it stores cyphertext. 9 | pub enum Multibox { 10 | // https://ssbc.github.io/scuttlebutt-protocol-guide/#private-messages 11 | PrivateBox(Vec), 12 | Other(u64, Vec), 13 | } 14 | 15 | impl Multibox { 16 | /// Creates a new private box multibox with the given secret text (*not* base64 encoded). 17 | pub fn new_private_box(secret: Vec) -> Multibox { 18 | Multibox::PrivateBox(secret) 19 | } 20 | 21 | /// Creates a multibox with the given identifier and the given secret text (*not* base64 encoded). 22 | pub fn new_multibox(id: u64, secret: Vec) -> Multibox { 23 | match id { 24 | 0 => Multibox::new_private_box(secret), 25 | _ => Multibox::Other(id, secret), 26 | } 27 | } 28 | 29 | /// Parses a 30 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox-legacy-encoding) 31 | /// into a `Multibox`, also returning the remaining input on success. 32 | pub fn from_legacy(s: &[u8]) -> Result<(Multibox, &[u8]), DecodeLegacyError> { 33 | let (data, suffix) = split_at_byte(s, 0x2E).ok_or(DecodeLegacyError::NoDot)?; 34 | 35 | base64::decode_config(data, base64::STANDARD) 36 | .map_err(DecodeLegacyError::InvalidBase64) 37 | .and_then(|cypher_raw| { 38 | if data.len() % 4 != 0 { 39 | return Err(DecodeLegacyError::NoncanonicPadding); 40 | } 41 | 42 | let tail = skip_prefix(suffix, b"box").ok_or(DecodeLegacyError::InvalidSuffix)?; 43 | 44 | match decode_base32_id(tail).ok_or(DecodeLegacyError::InvalidSuffix)? { 45 | (0, tail) => Ok((Multibox::PrivateBox(cypher_raw), tail)), 46 | (id, tail) => Ok((Multibox::Other(id, cypher_raw), tail)), 47 | } 48 | }) 49 | } 50 | 51 | /// Serialize a `Multibox` into a writer, using the 52 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox-legacy-encoding). 53 | pub fn to_legacy(&self, w: &mut W) -> Result<(), io::Error> { 54 | match self { 55 | Multibox::PrivateBox(ref bytes) => { 56 | let data = base64::encode_config(bytes, base64::STANDARD); 57 | w.write_all(data.as_bytes())?; 58 | 59 | w.write_all(b".box") 60 | } 61 | 62 | Multibox::Other(id, ref bytes) => { 63 | let data = base64::encode_config(bytes, base64::STANDARD); 64 | w.write_all(data.as_bytes())?; 65 | 66 | w.write_all(b".box")?; 67 | w.write_all(&encode_base32_id(*id)[..]) 68 | } 69 | } 70 | } 71 | 72 | /// Serialize a `Multibox` into an owned byte vector, using the 73 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox-legacy-encoding). 74 | pub fn to_legacy_vec(&self) -> Vec { 75 | let capacity = match self { 76 | Multibox::PrivateBox(ref cyphertext) => ((cyphertext.len() * 4) / 3) + 4, 77 | Multibox::Other(id, ref cyphertext) => { 78 | ((cyphertext.len() * 4) / 3) + 4 + id_len_base32(*id) 79 | } 80 | }; 81 | 82 | let mut out = Vec::with_capacity(capacity); 83 | self.to_legacy(&mut out).unwrap(); 84 | out 85 | } 86 | 87 | /// Serialize a `Multibox` into an owned string, using the 88 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multibox-legacy-encoding). 89 | pub fn to_legacy_string(&self) -> String { 90 | unsafe { String::from_utf8_unchecked(self.to_legacy_vec()) } 91 | } 92 | } 93 | 94 | /// Everything that can go wrong when decoding a `Multibox` from the legacy encoding. 95 | #[derive(Debug, PartialEq, Eq, Clone)] 96 | pub enum DecodeLegacyError { 97 | /// Input did not contain a `"."` to separate the data from the suffix. 98 | NoDot, 99 | /// The base64 portion of the box was invalid. 100 | InvalidBase64(base64::DecodeError), 101 | /// The base64 portion of the box did not use the correct amount of padding. 102 | NoncanonicPadding, 103 | /// The suffix is not well-formed. 104 | InvalidSuffix, 105 | } 106 | 107 | impl fmt::Display for DecodeLegacyError { 108 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 109 | match self { 110 | DecodeLegacyError::InvalidBase64(ref err) => write!(f, "{}", err), 111 | DecodeLegacyError::NoncanonicPadding => write!(f, "Incorrect number of padding '='s"), 112 | DecodeLegacyError::NoDot => write!(f, "No dot"), 113 | DecodeLegacyError::InvalidSuffix => write!(f, "Invalid suffix"), 114 | } 115 | } 116 | } 117 | 118 | impl std::error::Error for DecodeLegacyError {} 119 | 120 | // Decode the legacy format id of a multibox (canonic crockford base32, no leading zeros, at most 2^64 - 1). 121 | // Stops decoding when encountering end of input, a non-base32 character, or at the maximum identifier length. 122 | // In all these cases, it returns `Some(decoded)`, `None` is only returned if the first input 123 | // character is a zero or if a large identifier has a non-canonical first character. 124 | fn decode_base32_id(s: &[u8]) -> Option<(u64, &[u8])> { 125 | if s.get(0) == Some(&0x30) { 126 | return None; // Id may not begin with a zero. 127 | } 128 | 129 | let mut acc: u64 = 0; // The id is built up in this variable. 130 | 131 | for i in 0..13 { 132 | // 13 is the maximum length of an identifier 133 | match s.get(i) { 134 | None => return Some((acc, &[][..])), // end of input 135 | Some(c) => { 136 | if i == 12 && s[0] > 0x46 { 137 | // Noncanonical first character. 138 | return None; 139 | } 140 | 141 | let dec = match c { 142 | 0x30 => 0, 143 | 0x31 => 1, 144 | 0x32 => 2, 145 | 0x33 => 3, 146 | 0x34 => 4, 147 | 0x35 => 5, 148 | 0x36 => 6, 149 | 0x37 => 7, 150 | 0x38 => 8, 151 | 0x39 => 9, 152 | 0x41 => 10, 153 | 0x42 => 11, 154 | 0x43 => 12, 155 | 0x44 => 13, 156 | 0x45 => 14, 157 | 0x46 => 15, 158 | 0x47 => 16, 159 | 0x48 => 17, 160 | 0x4A => 18, 161 | 0x4B => 19, 162 | 0x4D => 20, 163 | 0x4E => 21, 164 | 0x50 => 22, 165 | 0x51 => 23, 166 | 0x52 => 24, 167 | 0x53 => 25, 168 | 0x54 => 26, 169 | 0x56 => 27, 170 | 0x57 => 28, 171 | 0x58 => 29, 172 | 0x59 => 30, 173 | 0x5A => 31, 174 | _ => return Some((acc, &s[i..])), // non-base32 input byte 175 | }; 176 | acc <<= 5; 177 | acc += dec; 178 | } 179 | } 180 | } 181 | // Reached maximum length of an identifier, return the decoded value and the remaining input. 182 | Some((acc, &s[13..])) 183 | } 184 | 185 | fn id_len_base32(id: u64) -> usize { 186 | (68 - id.leading_zeros() as usize) / 5 187 | } 188 | 189 | // Produces the canonical base32 encoding used for legacy multibox identifiers. 190 | fn encode_base32_id(id: u64) -> Vec { 191 | let len = id_len_base32(id); // how many bytes of output will this create? 192 | let mut out = Vec::with_capacity(len); 193 | 194 | for i in 0..len { 195 | let offset = ((len - 1) - i) * 5; // offset to the least-significant bit of the five bits to encode. 196 | let to_encode = (id >> offset) & 0b11111; // the five bits to encode (and leading zeros) 197 | 198 | // the symbol to write to the output 199 | let symbol = match to_encode { 200 | 0 => 0x30, 201 | 1 => 0x31, 202 | 2 => 0x32, 203 | 3 => 0x33, 204 | 4 => 0x34, 205 | 5 => 0x35, 206 | 6 => 0x36, 207 | 7 => 0x37, 208 | 8 => 0x38, 209 | 9 => 0x39, 210 | 10 => 0x41, 211 | 11 => 0x42, 212 | 12 => 0x43, 213 | 13 => 0x44, 214 | 14 => 0x45, 215 | 15 => 0x46, 216 | 16 => 0x47, 217 | 17 => 0x48, 218 | 18 => 0x4A, 219 | 19 => 0x4B, 220 | 20 => 0x4D, 221 | 21 => 0x4E, 222 | 22 => 0x50, 223 | 23 => 0x51, 224 | 24 => 0x52, 225 | 25 => 0x53, 226 | 26 => 0x54, 227 | 27 => 0x56, 228 | 28 => 0x57, 229 | 29 => 0x58, 230 | 30 => 0x59, 231 | 31 => 0x5A, 232 | _ => unreachable!(), 233 | }; 234 | 235 | out.push(symbol); 236 | } 237 | 238 | out 239 | } 240 | 241 | #[test] 242 | fn test_from_legacy() { 243 | assert!(Multibox::from_legacy(b"lB==.box").is_err()); 244 | assert!(Multibox::from_legacy(b"lA==.box0").is_err()); 245 | assert!(Multibox::from_legacy(b"lA==.box01").is_err()); 246 | assert!(Multibox::from_legacy(b"lA==.boxG0123456789AB").is_err()); 247 | assert!(Multibox::from_legacy(b"lA==.boxF0123456789AB").is_ok()); 248 | 249 | match Multibox::from_legacy(b".box").unwrap().0 { 250 | Multibox::PrivateBox(data) => assert_eq!(data.len(), 0), 251 | _ => panic!(), 252 | } 253 | 254 | assert_matches!( 255 | Multibox::from_legacy(b"lA==.box").unwrap().0, 256 | Multibox::PrivateBox(..) 257 | ); 258 | assert_matches!( 259 | Multibox::from_legacy(b"lA==.boxa").unwrap().0, 260 | Multibox::PrivateBox(..) 261 | ); 262 | assert_matches!( 263 | Multibox::from_legacy(b"lA==.boxU").unwrap().0, 264 | Multibox::PrivateBox(..) 265 | ); 266 | assert_matches!( 267 | Multibox::from_legacy(b"lA==.box\"").unwrap().0, 268 | Multibox::PrivateBox(..) 269 | ); 270 | assert_matches!( 271 | Multibox::from_legacy(b"lA==.box1").unwrap().0, 272 | Multibox::Other(1, _) 273 | ); 274 | assert_matches!( 275 | Multibox::from_legacy(b"lA==.boxV").unwrap().0, 276 | Multibox::Other(27, _) 277 | ); 278 | assert_matches!( 279 | Multibox::from_legacy(b"lA==.box11").unwrap().0, 280 | Multibox::Other(0b00001_00001, _) 281 | ); 282 | assert_matches!( 283 | Multibox::from_legacy(b".boxNN").unwrap().0, 284 | Multibox::Other(0b10101_10101, _) 285 | ); 286 | } 287 | 288 | #[test] 289 | fn test_to_legacy() { 290 | assert_eq!(Multibox::new_private_box(vec![]).to_legacy_vec(), b".box"); 291 | assert_eq!( 292 | Multibox::new_multibox(0b10101_10101, vec![]).to_legacy_vec(), 293 | b".boxNN" 294 | ); 295 | } 296 | -------------------------------------------------------------------------------- /src/multifeed.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use super::multikey::{self, Multikey}; 4 | 5 | /// A multifeed that owns its data. 6 | #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)] 7 | pub enum Multifeed { 8 | Multikey(Multikey), 9 | } 10 | 11 | impl Multifeed { 12 | /// Create a new multifeed of kind `multikey`. 13 | pub fn from_multikey(mk: Multikey) -> Multifeed { 14 | Multifeed::Multikey(mk) 15 | } 16 | 17 | /// Parses a 18 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multifeed-legacy-encoding) 19 | /// into a `Multifeed`, also returning the remaining input on success. 20 | pub fn from_legacy(s: &[u8]) -> Result<(Multifeed, &[u8]), DecodeLegacyError> { 21 | if s.is_empty() { 22 | return Err(DecodeLegacyError::UnknownKind); 23 | } 24 | 25 | match s[0] { 26 | 0x40 => { 27 | let (mk, tail) = Multikey::from_legacy(s)?; 28 | Ok((Multifeed::from_multikey(mk), tail)) 29 | } 30 | _ => Err(DecodeLegacyError::UnknownKind), 31 | } 32 | } 33 | 34 | /// Serialize a `Multifeed` into a writer, using the 35 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multifeed-legacy-encoding). 36 | pub fn to_legacy(&self, w: &mut W) -> Result<(), io::Error> { 37 | match self { 38 | Multifeed::Multikey(ref mk) => mk.to_legacy(w), 39 | } 40 | } 41 | 42 | /// Serialize a `Multifeed` into an owned byte vector, using the 43 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multifeed-legacy-encoding). 44 | pub fn to_legacy_vec(&self) -> Vec { 45 | match self { 46 | Multifeed::Multikey(ref mk) => mk.to_legacy_vec(), 47 | } 48 | } 49 | 50 | /// Serialize a `Multifeed` into an owned string, using the 51 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multifeed-legacy-encoding). 52 | pub fn to_legacy_string(&self) -> String { 53 | unsafe { String::from_utf8_unchecked(self.to_legacy_vec()) } 54 | } 55 | } 56 | 57 | /// Everything that can go wrong when decoding a `Multikey` from the legacy encoding. 58 | #[derive(Debug, PartialEq, Eq, Clone)] 59 | pub enum DecodeLegacyError { 60 | /// Input did not start with the `"@"` sigil. 61 | UnknownKind, 62 | /// Decoding the inner multikey failed. 63 | Multikey(multikey::DecodeLegacyError), 64 | } 65 | 66 | impl From for DecodeLegacyError { 67 | fn from(err: multikey::DecodeLegacyError) -> DecodeLegacyError { 68 | DecodeLegacyError::Multikey(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/multihash.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of [ssb multihashes](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash). 2 | use std::fmt; 3 | use std::io::{self, Write}; 4 | 5 | use serde::{ 6 | de::{Deserialize, Deserializer, Error}, 7 | ser::{Serialize, Serializer}, 8 | }; 9 | 10 | use super::{base64, serde, skip_prefix, split_at_byte}; 11 | 12 | /// A multihash that owns its data. 13 | #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)] 14 | pub enum Multihash { 15 | /// An ssb [message](https://spec.scuttlebutt.nz/feed/messages.html). 16 | Message([u8; 32]), 17 | /// An ssb [blob](TODO). 18 | Blob([u8; 32]), 19 | } 20 | 21 | enum Target { 22 | Message, 23 | Blob, 24 | } 25 | 26 | impl Multihash { 27 | /// Parses a 28 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash-legacy-encoding) 29 | /// into a `Multihash`. 30 | pub fn from_legacy(mut s: &[u8]) -> Result<(Multihash, &[u8]), DecodeLegacyError> { 31 | let target; 32 | 33 | if let Some(tail) = skip_prefix(s, b"%") { 34 | s = tail; 35 | target = Target::Message; 36 | } else { 37 | let tail = skip_prefix(s, b"&").ok_or(DecodeLegacyError::Sigil)?; 38 | 39 | s = tail; 40 | target = Target::Blob; 41 | } 42 | 43 | let (data, suffix) = split_at_byte(s, 0x2E).ok_or(DecodeLegacyError::NoDot)?; 44 | 45 | let tail = skip_prefix(suffix, SHA256_SUFFIX).ok_or(DecodeLegacyError::UnknownSuffix)?; 46 | 47 | if data.len() != SHA256_BASE64_LEN { 48 | return Err(DecodeLegacyError::Sha256WrongSize); 49 | } 50 | 51 | if data[SHA256_BASE64_LEN - 2] == b"="[0] { 52 | return Err(DecodeLegacyError::Sha256WrongSize); 53 | } 54 | 55 | if data[SHA256_BASE64_LEN - 1] != b"="[0] { 56 | return Err(DecodeLegacyError::Sha256WrongSize); 57 | } 58 | 59 | let mut dec_data = [0_u8; 32]; 60 | base64::decode_config_slice(data, base64::STANDARD, &mut dec_data[..]) 61 | .map_err(DecodeLegacyError::InvalidBase64) 62 | .map(|_| { 63 | let multihash = match target { 64 | Target::Blob => Multihash::Blob(dec_data), 65 | Target::Message => Multihash::Message(dec_data), 66 | }; 67 | (multihash, tail) 68 | }) 69 | } 70 | 71 | /// Serialize a `Multihash` into a writer, using the 72 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash-legacy-encoding). 73 | pub fn to_legacy(&self, w: &mut W) -> Result<(), io::Error> { 74 | match self { 75 | Multihash::Message(ref bytes) => { 76 | w.write_all(b"%")?; 77 | Multihash::write_legacy_hash_and_suffix(bytes, w) 78 | } 79 | Multihash::Blob(ref bytes) => { 80 | w.write_all(b"&")?; 81 | Multihash::write_legacy_hash_and_suffix(bytes, w) 82 | } 83 | } 84 | } 85 | 86 | fn write_legacy_hash_and_suffix(bytes: &[u8], w: &mut W) -> Result<(), io::Error> { 87 | let data = base64::encode_config(bytes, base64::STANDARD); 88 | w.write_all(data.as_bytes())?; 89 | 90 | w.write_all(b".")?; 91 | w.write_all(SHA256_SUFFIX) 92 | } 93 | 94 | /// Serialize a `Multihash` into an owned byte vector, using the 95 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash-legacy-encoding). 96 | pub fn to_legacy_vec(&self) -> Vec { 97 | let mut out = Vec::with_capacity(SSB_SHA256_ENCODED_LEN); 98 | self.to_legacy(&mut out).unwrap(); 99 | out 100 | } 101 | 102 | /// Serialize a `Multihash` into an owned string, using the 103 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multihash-legacy-encoding). 104 | pub fn to_legacy_string(&self) -> String { 105 | unsafe { String::from_utf8_unchecked(self.to_legacy_vec()) } 106 | } 107 | } 108 | 109 | impl Serialize for Multihash { 110 | fn serialize(&self, serializer: S) -> Result 111 | where 112 | S: Serializer, 113 | { 114 | serializer.serialize_str(&self.to_legacy_string()) 115 | } 116 | } 117 | 118 | impl<'de> Deserialize<'de> for Multihash { 119 | fn deserialize(deserializer: D) -> Result 120 | where 121 | D: Deserializer<'de>, 122 | { 123 | let s = String::deserialize(deserializer)?; 124 | Multihash::from_legacy(s.as_bytes()) 125 | .map(|(mh, _)| mh) 126 | .map_err(|err| D::Error::custom(format!("Invalid multihash: {}", err))) 127 | } 128 | } 129 | 130 | /// Everything that can go wrong when decoding a `Multihash` from the legacy encoding. 131 | #[derive(Debug, PartialEq, Eq, Clone)] 132 | pub enum DecodeLegacyError { 133 | /// Input did not start with the `"@"` sigil. 134 | Sigil, 135 | /// Input did not contain a `"."` to separate the data from the suffix. 136 | NoDot, 137 | /// The base64 portion of the key was invalid. 138 | InvalidBase64(base64::DecodeError), 139 | /// The suffix is not known to this ssb implementation. 140 | UnknownSuffix, 141 | /// The suffix declares a sha256 hash, but the data length does not match. 142 | Sha256WrongSize, 143 | } 144 | 145 | impl fmt::Display for DecodeLegacyError { 146 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 147 | match self { 148 | DecodeLegacyError::Sigil => write!(f, "Invalid sigil"), 149 | DecodeLegacyError::InvalidBase64(ref err) => write!(f, "{}", err), 150 | DecodeLegacyError::NoDot => write!(f, "No dot"), 151 | DecodeLegacyError::UnknownSuffix => write!(f, "Unknown suffix"), 152 | DecodeLegacyError::Sha256WrongSize => write!(f, "Data of wrong length"), 153 | } 154 | } 155 | } 156 | 157 | impl std::error::Error for DecodeLegacyError {} 158 | 159 | /// The legacy suffix indicating the sha256 cryptographic primitive. 160 | const SHA256_SUFFIX: &[u8] = b"sha256"; 161 | /// Length of a base64 encoded sha256 hash digest. 162 | const SHA256_BASE64_LEN: usize = 44; 163 | /// Length of a legacy-encoded ssb `Multihash` which uses the sha256 cryptographic primitive. 164 | const SSB_SHA256_ENCODED_LEN: usize = SHA256_BASE64_LEN + 9; 165 | 166 | #[test] 167 | fn test_from_legacy() { 168 | assert!( 169 | Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=.sha256").is_ok() 170 | ); 171 | assert!( 172 | Multihash::from_legacy(b"&MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=.sha256").is_ok() 173 | ); 174 | assert!( 175 | Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rd=.sha256").is_err() 176 | ); 177 | assert!( 178 | Multihash::from_legacy(b"@MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=.sha256").is_err() 179 | ); 180 | assert!( 181 | Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=.tha256").is_err() 182 | ); 183 | assert!( 184 | Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc=sha256").is_err() 185 | ); 186 | assert!( 187 | Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc.sha256").is_err() 188 | ); 189 | assert!( 190 | Multihash::from_legacy(b"%MwjdLV95P7VqHfrgS49nScXsyIwJfL229e5OSKc+0rc==.sha256").is_err() 191 | ); 192 | } 193 | -------------------------------------------------------------------------------- /src/multikey.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of [ssb multikeys](https://spec.scuttlebutt.nz/feed/datatypes.html#multikey). 2 | use std::cmp::{Eq, Ord, PartialEq, PartialOrd}; 3 | use std::fmt; 4 | use std::io::{self, Cursor, Write}; 5 | 6 | use crate::{skip_prefix, split_at_byte}; 7 | use serde::{ 8 | de::{Deserialize, Deserializer, Error}, 9 | ser::{Serialize, Serializer}, 10 | }; 11 | 12 | use ssb_crypto::{AsBytes, Keypair, PublicKey, Signature}; 13 | 14 | /// A multikey that owns its data. 15 | #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)] 16 | pub enum Multikey { 17 | // An [ed25519](http://ed25519.cr.yp.to/) public key. 18 | Ed25519(PublicKey), 19 | } 20 | 21 | impl Multikey { 22 | /// Take an ed25519 public key and turn it into an opaque `Multikey`. 23 | pub fn from_ed25519(pk: &[u8; 32]) -> Multikey { 24 | Multikey::Ed25519(PublicKey::from_slice(pk).unwrap()) 25 | } 26 | 27 | pub fn from_ed25519_slice(pk: &[u8]) -> Multikey { 28 | Multikey::Ed25519(PublicKey::from_slice(pk).unwrap()) 29 | } 30 | 31 | pub fn into_ed25519_public_key(self) -> Option { 32 | match self { 33 | Multikey::Ed25519(pk) => Some(pk), 34 | } 35 | } 36 | 37 | /// Parses a 38 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multikey-legacy-encoding) 39 | /// into a `Multikey`, also returning the remaining input on success. 40 | pub fn from_legacy(mut s: &[u8]) -> Result<(Multikey, &[u8]), DecodeLegacyError> { 41 | s = skip_prefix(s, b"@").ok_or(DecodeLegacyError::Sigil)?; 42 | 43 | let (data, suffix) = split_at_byte(s, 0x2E).ok_or(DecodeLegacyError::NoDot)?; 44 | 45 | let tail = skip_prefix(suffix, ED25519_SUFFIX).ok_or(DecodeLegacyError::UnknownSuffix)?; 46 | 47 | if data.len() != ED25519_PK_BASE64_LEN { 48 | return Err(DecodeLegacyError::Ed25519WrongSize); 49 | } 50 | 51 | if data[ED25519_PK_BASE64_LEN - 2] == b"="[0] { 52 | return Err(DecodeLegacyError::Ed25519WrongSize); 53 | } 54 | 55 | if data[ED25519_PK_BASE64_LEN - 1] != b"="[0] { 56 | return Err(DecodeLegacyError::Ed25519WrongSize); 57 | } 58 | 59 | let mut dec_data = [0_u8; 32]; 60 | 61 | base64::decode_config_slice(data, base64::STANDARD, &mut dec_data) 62 | .map_err(|_| DecodeLegacyError::InvalidBase64) 63 | .map(|_| (Multikey::from_ed25519(&dec_data), tail)) 64 | } 65 | 66 | /// Serialize a `Multikey` into a writer, using the 67 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multikey-legacy-encoding). 68 | pub fn to_legacy(&self, w: &mut W) -> Result<(), io::Error> { 69 | match self { 70 | Multikey::Ed25519(ref pk) => { 71 | w.write_all(b"@")?; 72 | 73 | let data = pk.as_base64(); 74 | w.write_all(data.as_bytes())?; 75 | 76 | w.write_all(b".")?; 77 | w.write_all(ED25519_SUFFIX) 78 | } 79 | } 80 | } 81 | 82 | /// Serialize a `Multikey` into an owned byte vector, using the 83 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multikey-legacy-encoding). 84 | pub fn to_legacy_vec(&self) -> Vec { 85 | let mut data = vec![]; 86 | self.to_legacy(&mut data).unwrap(); 87 | data 88 | } 89 | 90 | /// Serialize a `Multikey` into an owned string, using the 91 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multikey-legacy-encoding). 92 | pub fn to_legacy_string(&self) -> String { 93 | String::from_utf8(self.to_legacy_vec()).unwrap() 94 | } 95 | 96 | /// Check whether the given signature of the given text was created by this key. 97 | pub fn is_signature_correct(&self, data: &[u8], sig: &Multisig) -> bool { 98 | match (&self, &sig.0) { 99 | (Multikey::Ed25519(ref pk), _Multisig::Ed25519(ref sig)) => pk.verify(sig, data), 100 | } 101 | } 102 | } 103 | 104 | impl Serialize for Multikey { 105 | fn serialize(&self, serializer: S) -> Result 106 | where 107 | S: Serializer, 108 | { 109 | serializer.serialize_str(&self.to_legacy_string()) 110 | } 111 | } 112 | 113 | impl<'de> Deserialize<'de> for Multikey { 114 | fn deserialize(deserializer: D) -> Result 115 | where 116 | D: Deserializer<'de>, 117 | { 118 | let s = String::deserialize(deserializer)?; 119 | Multikey::from_legacy(s.as_bytes()) 120 | .map(|(mk, _)| mk) 121 | .map_err(|err| D::Error::custom(format!("Invalid multikey: {}", err))) 122 | } 123 | } 124 | 125 | /// Everything that can go wrong when decoding a `Multikey` from the legacy encoding. 126 | #[derive(Debug, PartialEq, Eq, Clone)] 127 | pub enum DecodeLegacyError { 128 | /// Input did not start with the `"@"` sigil. 129 | Sigil, 130 | /// Input did not contain a `"."` to separate the data from the suffix. 131 | NoDot, 132 | /// Invalid utf8 string. 133 | InvalidUTF8, 134 | /// The base64 portion of the key was invalid. 135 | InvalidBase64, 136 | /// The suffix is not known to this ssb implementation. 137 | UnknownSuffix, 138 | /// The suffix declares an ed25519 key, but the data length does not match. 139 | Ed25519WrongSize, 140 | } 141 | 142 | impl fmt::Display for DecodeLegacyError { 143 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 144 | match self { 145 | DecodeLegacyError::Sigil => write!(f, "Invalid sigil"), 146 | DecodeLegacyError::InvalidUTF8 => write!(f, "Invalid utf8"), 147 | DecodeLegacyError::InvalidBase64 => write!(f, "Invalid base64"), 148 | DecodeLegacyError::NoDot => write!(f, "No dot"), 149 | DecodeLegacyError::UnknownSuffix => write!(f, "Unknown suffix"), 150 | DecodeLegacyError::Ed25519WrongSize => write!(f, "Data of wrong length"), 151 | } 152 | } 153 | } 154 | 155 | impl std::error::Error for DecodeLegacyError {} 156 | 157 | /// The secret counterpart to Multikey. 158 | #[derive(Debug, Clone)] 159 | pub struct Multisecret(Keypair); 160 | 161 | impl Multisecret { 162 | /// Parses a 163 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multikey-legacy-encoding) 164 | /// into a `Multisecret`, also returning the remaining input on success. 165 | pub fn from_legacy(s: &[u8]) -> Result<(Multisecret, &[u8]), DecodeLegacyError> { 166 | let (data, suffix) = split_at_byte(s, 0x2E).ok_or(DecodeLegacyError::NoDot)?; 167 | 168 | let tail = skip_prefix(suffix, ED25519_SUFFIX).ok_or(DecodeLegacyError::UnknownSuffix)?; 169 | 170 | let data_str = std::str::from_utf8(data).map_err(|_| DecodeLegacyError::InvalidUTF8)?; 171 | 172 | let key_pair = Keypair::from_base64(data_str).ok_or(DecodeLegacyError::InvalidBase64)?; 173 | 174 | Ok((Multisecret(key_pair), tail)) 175 | } 176 | 177 | /// Serialize a `Multisecret` into a writer, using the 178 | /// [legacy encoding](https://spec.scuttlebutt.nz/feed/datatypes.html#multikey-legacy-encoding). 179 | pub fn to_legacy(&self, w: &mut W) -> Result<(), io::Error> { 180 | let data = self.0.as_base64(); 181 | w.write_all(data.as_bytes())?; 182 | w.write_all(b".")?; 183 | w.write_all(ED25519_SUFFIX) 184 | } 185 | } 186 | 187 | impl Serialize for Multisecret { 188 | fn serialize(&self, serializer: S) -> Result 189 | where 190 | S: Serializer, 191 | { 192 | let mut s = [0_u8; SSB_ED25519_SECRET_ENCODED_LEN]; 193 | self.to_legacy(&mut Cursor::new(&mut s[..])).unwrap(); 194 | serializer.serialize_str(std::str::from_utf8(&s).unwrap()) 195 | } 196 | } 197 | 198 | impl<'de> Deserialize<'de> for Multisecret { 199 | fn deserialize(deserializer: D) -> Result 200 | where 201 | D: Deserializer<'de>, 202 | { 203 | let s = String::deserialize(deserializer)?; 204 | Multisecret::from_legacy(s.as_bytes()) 205 | .map(|(mk, _)| mk) 206 | .map_err(|err| D::Error::custom(format!("Invalid multikey: {}", err))) 207 | } 208 | } 209 | 210 | /// A signature that owns its data. 211 | #[derive(Debug, PartialEq, Eq, Clone)] 212 | pub struct Multisig(_Multisig); 213 | 214 | #[derive(Clone)] 215 | enum _Multisig { 216 | // An [ed25519](http://ed25519.cr.yp.to/) signature. 217 | Ed25519(Signature), 218 | } 219 | 220 | impl fmt::Debug for _Multisig { 221 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 222 | match self { 223 | _Multisig::Ed25519(data) => write!(f, "Ed25519 signature: {:?}", data.as_bytes()), 224 | } 225 | } 226 | } 227 | 228 | impl PartialEq for _Multisig { 229 | fn eq(&self, other: &_Multisig) -> bool { 230 | match (self, other) { 231 | (_Multisig::Ed25519(ref a), _Multisig::Ed25519(ref b)) => a.as_bytes() == b.as_bytes(), 232 | } 233 | } 234 | } 235 | 236 | impl Eq for _Multisig {} 237 | 238 | impl Multikey { 239 | /// Deserialize a legacy signature corresponding to this key type. 240 | pub fn sig_from_legacy<'a>( 241 | &self, 242 | s: &'a [u8], 243 | ) -> Result<(Multisig, &'a [u8]), DecodeSignatureError> { 244 | let (data, suffix) = split_at_byte(s, 0x2E).ok_or(DecodeSignatureError::NoDot)?; 245 | 246 | let suffix = skip_prefix(suffix, b"sig").ok_or(DecodeSignatureError::NoDotSig)?; 247 | 248 | match self { 249 | Multikey::Ed25519(_) => { 250 | let tail = 251 | skip_prefix(suffix, b".ed25519").ok_or(DecodeSignatureError::UnknownSuffix)?; 252 | 253 | if data.len() != ED25519_SIG_BASE64_LEN { 254 | return Err(DecodeSignatureError::Ed25519WrongSize); 255 | } 256 | 257 | if data[ED25519_SIG_BASE64_LEN - 2] != b"="[0] { 258 | return Err(DecodeSignatureError::Ed25519WrongSize); 259 | } 260 | 261 | let mut dec_data = [0_u8; 64]; 262 | 263 | base64::decode_config_slice(data, base64::STANDARD, &mut dec_data[..]) 264 | .map_err(DecodeSignatureError::InvalidBase64) 265 | .map(|_| (Multisig::from_ed25519(&dec_data), tail)) 266 | } 267 | } 268 | } 269 | } 270 | 271 | impl Multisig { 272 | /// Take an ed25519 signature and turn it into an opaque `Multisig`. 273 | pub fn from_ed25519(sig: &[u8; 64]) -> Multisig { 274 | Multisig(_Multisig::Ed25519(Signature::from_slice(sig).unwrap())) 275 | } 276 | 277 | /// Serialize a signature into a writer, in the appropriate 278 | /// form for a [legacy message](https://spec.scuttlebutt.nz/feed/messages.html#legacy-json-encoding). 279 | pub fn to_legacy(&self, w: &mut W) -> Result<(), io::Error> { 280 | match self.0 { 281 | _Multisig::Ed25519(ref sig) => { 282 | let data = sig.as_base64(); 283 | w.write_all(data.as_bytes())?; 284 | w.write_all(b".sig.ed25519") 285 | } 286 | } 287 | } 288 | 289 | /// Serialize a signature into an owned byte vector, 290 | /// in the appropriate form for a 291 | /// [legacy message](https://spec.scuttlebutt.nz/feed/messages.html#legacy-json-encoding). 292 | pub fn to_legacy_vec(&self) -> Vec { 293 | let mut data = vec![]; 294 | self.to_legacy(&mut data).unwrap(); 295 | data 296 | } 297 | 298 | /// Serialize a signature into an owned string, 299 | /// in the appropriate form for a 300 | /// [legacy message](https://spec.scuttlebutt.nz/feed/messages.html#legacy-json-encoding). 301 | pub fn to_legacy_string(&self) -> String { 302 | String::from_utf8(self.to_legacy_vec()).unwrap() 303 | } 304 | } 305 | 306 | /// Everything that can go wrong when decoding a signature from the legacy encoding. 307 | #[derive(Debug, PartialEq, Eq, Clone)] 308 | pub enum DecodeSignatureError { 309 | /// Input did not contain a `"."` to separate the data from the suffix. 310 | NoDot, 311 | /// Input did contain the mandatory ".sig". 312 | NoDotSig, 313 | /// The base64 portion of the key was invalid. 314 | InvalidBase64(base64::DecodeError), 315 | /// The suffix is not known to this ssb implementation. 316 | UnknownSuffix, 317 | /// The suffix declares an ed25519 signature, but the data length does not match. 318 | Ed25519WrongSize, 319 | } 320 | 321 | impl fmt::Display for DecodeSignatureError { 322 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 323 | match self { 324 | DecodeSignatureError::InvalidBase64(ref err) => write!(f, "{}", err), 325 | DecodeSignatureError::NoDot => write!(f, "No dot"), 326 | DecodeSignatureError::NoDotSig => write!(f, "No .sig"), 327 | DecodeSignatureError::UnknownSuffix => write!(f, "Unknown suffix"), 328 | DecodeSignatureError::Ed25519WrongSize => write!(f, "Data of wrong length"), 329 | } 330 | } 331 | } 332 | 333 | impl std::error::Error for DecodeSignatureError {} 334 | 335 | /// The legacy suffix indicating the ed25519 cryptographic primitive. 336 | const ED25519_SUFFIX: &[u8] = b"ed25519"; 337 | /// Length of a base64 encoded ed25519 public key. 338 | const ED25519_PK_BASE64_LEN: usize = 44; 339 | /// Length of a base64 encoded ed25519 public key. 340 | const ED25519_SIG_BASE64_LEN: usize = 88; 341 | /// Length of a legacy-encoded ssb ed25519 secret key. 342 | const SSB_ED25519_SECRET_ENCODED_LEN: usize = 96; 343 | 344 | #[test] 345 | fn test_from_legacy() { 346 | let valid_key = b"@zurF8X68ArfRM71dF3mKh36W0xDM8QmOnAS5bYOq8hA=.ed25519"; 347 | let (key, _) = Multikey::from_legacy(valid_key).unwrap(); 348 | let legacy_key = key.to_legacy_vec(); 349 | 350 | assert_eq!(legacy_key, valid_key); 351 | 352 | assert!( 353 | Multikey::from_legacy(b"@zurF8X68ArfRM71dF3mKh36W0xDM8QmOnAS5bYOq8hB=.ed25519").is_err() 354 | ); 355 | assert!( 356 | Multikey::from_legacy(b"&zurF8X68ArfRM71dF3mKh36W0xDM8QmOnAS5bYOq8hA=.ed25519").is_err() 357 | ); 358 | assert!( 359 | Multikey::from_legacy(b"@zurF8X68ArfRM71dF3mKh36W0xDM8QmOnAS5bYOq8hA=.dd25519").is_err() 360 | ); 361 | assert!( 362 | Multikey::from_legacy(b"@zurF8X68ArfRM71dF3mKh36W0xDM8QmOnAS5bYOq8hA=ed25519").is_err() 363 | ); 364 | assert!( 365 | Multikey::from_legacy(b"@zurF8X68ArfRM71dF3mKh36W0xDM8QmOnAS5bYOq8hA.ed25519").is_err() 366 | ); 367 | assert!( 368 | Multikey::from_legacy(b"@zurF8X68ArfRM71dF3mKh36W0xDM8QmOnAS5bYOq8hA==.ed25519").is_err() 369 | ); 370 | } 371 | --------------------------------------------------------------------------------