Skip to main content

lb_rs/model/
account.rs

1use crate::model::pubkey;
2use bip39_dict::Language;
3use libsecp256k1::{PublicKey, SecretKey};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::fmt::Write;
7
8use super::errors::{LbErrKind, LbResult};
9
10pub const MAX_USERNAME_LENGTH: usize = 32;
11
12/// A flag for which users have volunteers as beta testers.
13///
14/// Beta users are users to which riskier code is enabled first for testing.
15/// Beta users are also users who have opted into telemetry by way of approving a PR that adds
16/// their name to this list. Certainly telemetry in lockbook will always be opt in but the
17/// mechanism of consent may evolve over time.
18pub const BETA_USERS: &[&str] = &[
19    "parth",
20    "travis",
21    "smail",
22    "adam",
23    "krish",
24    "aravd",
25    "lucaloncar",
26    "krishma",
27    "steve",
28    "rahul",
29    "chetna",
30    "chefbowyer",
31    "raayan",
32    "praful",
33    "paulhovey",
34    "amumu",
35    "coreycole",
36];
37
38pub type Username = String;
39pub type ApiUrl = String;
40
41#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
42pub struct Account {
43    pub username: Username,
44    pub api_url: ApiUrl,
45    #[serde(with = "secret_key_serializer")]
46    pub private_key: SecretKey,
47}
48
49impl Account {
50    pub fn new(username: String, api_url: String) -> Self {
51        let private_key = pubkey::generate_key();
52        Self { username, api_url, private_key }
53    }
54
55    pub fn public_key(&self) -> PublicKey {
56        PublicKey::from_secret_key(&self.private_key)
57    }
58
59    pub fn get_phrase(&self) -> LbResult<[&'static str; 24]> {
60        let key = self.private_key.serialize();
61        let key_bits = key.iter().fold(String::new(), |mut out, byte| {
62            let _ = write!(out, "{byte:08b}");
63            out
64        });
65
66        let checksum: String =
67            sha2::Sha256::digest(&key)
68                .into_iter()
69                .fold(String::new(), |mut out, byte| {
70                    let _ = write!(out, "{byte:08b}");
71                    out
72                });
73
74        let checksum_last_4_bits = &checksum[..4];
75        let combined_bits = format!("{key_bits}{checksum_last_4_bits}");
76
77        let mut phrase: [&str; 24] = Default::default();
78
79        for (i, chunk) in combined_bits
80            .chars()
81            .collect::<Vec<_>>()
82            .chunks(11)
83            .enumerate()
84        {
85            let index =
86                u16::from_str_radix(&chunk.iter().collect::<String>(), 2).map_err(|_| {
87                    LbErrKind::Unexpected(
88                        "could not parse appropriate private key bits into u16".to_string(),
89                    )
90                })?;
91            let word = bip39_dict::ENGLISH.lookup_word(bip39_dict::MnemonicIndex(index));
92
93            phrase[i] = word;
94        }
95
96        Ok(phrase)
97    }
98
99    pub fn phrase_to_private_key(phrases: [&str; 24]) -> LbResult<SecretKey> {
100        let mut combined_bits = phrases
101            .iter()
102            .map(|word| {
103                bip39_dict::ENGLISH
104                    .lookup_mnemonic(word)
105                    .map(|index| format!("{:011b}", index.0))
106            })
107            .collect::<Result<String, _>>()
108            .map_err(|_| LbErrKind::KeyPhraseInvalid)?;
109
110        if combined_bits.len() != 264 {
111            return Err(LbErrKind::Unexpected("the number of bits after translating the phrase does not equal the expected amount (264)".to_string()).into());
112        }
113
114        for _ in 0..4 {
115            combined_bits.remove(253);
116        }
117
118        let key_bits = &combined_bits[..256];
119        let checksum_last_4_bits = &combined_bits[256..260];
120
121        let mut key: Vec<u8> = Vec::new();
122        for chunk in key_bits.chars().collect::<Vec<_>>().chunks(8) {
123            let comp = u8::from_str_radix(&chunk.iter().collect::<String>(), 2).map_err(|_| {
124                LbErrKind::Unexpected(
125                    "could not parse appropriate phrases bits into u8".to_string(),
126                )
127            })?;
128
129            key.push(comp);
130        }
131
132        let gen_checksum: String =
133            sha2::Sha256::digest(&key)
134                .iter()
135                .fold(String::new(), |mut acc, byte| {
136                    acc.push_str(&format!("{byte:08b}"));
137                    acc
138                });
139
140        let gen_checksum_last_4 = &gen_checksum[..4];
141
142        if gen_checksum_last_4 != checksum_last_4_bits {
143            return Err(LbErrKind::KeyPhraseInvalid)?;
144        }
145
146        Ok(SecretKey::parse_slice(&key).map_err(|e| {
147            error!("unexpected secretkey parse error: {e:?}");
148            LbErrKind::KeyPhraseInvalid
149        })?)
150    }
151
152    /// hashes the username and takes the first three bytes of the has as rgb values
153    /// the deterministic color experiment:
154    ///
155    /// anywhere in the app you see someone's name, a UI developer has the choice to show the
156    /// color associated with the username. As our platform doesn't have profile pictures this
157    /// serves as a secondary cue for identification of people you collaborate with frequently.
158    ///
159    /// imagine the blame view of a file color coded. If we can commit to not persisting this value
160    /// anywhere we can even experiment with more sophisticated color science. Maybe docs.rs
161    /// is when we can signal that this color is a stable value. I can see us doing a more HSL
162    /// based generation strategy.
163    ///
164    /// ultimately if this experiment fails we can explore having server persist this information.
165    pub fn color(&self) -> (u8, u8, u8) {
166        let mut hasher = Sha256::new();
167        hasher.update(&self.username);
168        let result = hasher.finalize();
169
170        (result[0], result[1], result[2])
171    }
172
173    pub fn is_beta(&self) -> bool {
174        BETA_USERS.contains(&self.username.as_str())
175    }
176}
177
178pub mod secret_key_serializer {
179    use libsecp256k1::SecretKey;
180    use serde::de::{Deserialize, Deserializer};
181    use serde::ser::Serializer;
182
183    pub fn serialize<S>(sk: &SecretKey, serializer: S) -> Result<S::Ok, S::Error>
184    where
185        S: Serializer,
186    {
187        serializer.serialize_bytes(&sk.serialize())
188    }
189
190    pub fn deserialize<'de, D>(deserializer: D) -> Result<SecretKey, D::Error>
191    where
192        D: Deserializer<'de>,
193    {
194        let key = <Vec<u8>>::deserialize(deserializer)?;
195        let sk = SecretKey::parse_slice(&key).map_err(serde::de::Error::custom)?;
196        Ok(sk)
197    }
198}
199
200#[cfg(test)]
201mod test_account_serialization {
202    use libsecp256k1::SecretKey;
203    use rand::rngs::OsRng;
204
205    use crate::model::account::Account;
206
207    #[test]
208    fn account_serialize_deserialize() {
209        let account1 = Account {
210            username: "test".to_string(),
211            api_url: "test.com".to_string(),
212            private_key: SecretKey::random(&mut OsRng),
213        };
214
215        let encoded: Vec<u8> = bincode::serialize(&account1).unwrap();
216        let account2: Account = bincode::deserialize(&encoded).unwrap();
217
218        assert_eq!(account1, account2);
219    }
220
221    #[test]
222    fn verify_account_v1() {
223        let account1 = Account {
224            username: "test1".to_string(),
225            api_url: "test1.com".to_string(),
226            private_key: SecretKey::parse_slice(&[
227                19, 34, 85, 4, 36, 83, 52, 122, 49, 107, 223, 44, 31, 16, 2, 160, 100, 103, 193, 0,
228                67, 15, 184, 133, 33, 111, 91, 143, 137, 232, 240, 42,
229            ])
230            .unwrap(),
231        };
232
233        let account2 = bincode::deserialize(
234            base64::decode("BQAAAAAAAAB0ZXN0MQkAAAAAAAAAdGVzdDEuY29tIAAAAAAAAAATIlUEJFM0ejFr3ywfEAKgZGfBAEMPuIUhb1uPiejwKg==")
235                .unwrap()
236                .as_slice()
237        ).unwrap();
238
239        assert_eq!(account1, account2);
240    }
241
242    #[test]
243    fn verify_account_v2() {
244        let account1 = Account {
245            username: "test1".to_string(),
246            api_url: "test1.com".to_string(),
247            private_key: SecretKey::parse_slice(&[
248                158, 250, 59, 72, 139, 112, 93, 137, 168, 199, 28, 230, 56, 37, 43, 52, 152, 176,
249                243, 149, 124, 11, 2, 126, 73, 118, 252, 112, 225, 207, 34, 90,
250            ])
251            .unwrap(),
252        };
253
254        let account2 = Account {
255            username: "test1".to_string(),
256            api_url: "test1.com".to_string(),
257            private_key: SecretKey::parse_slice(
258                base64::decode("nvo7SItwXYmoxxzmOCUrNJiw85V8CwJ+SXb8cOHPIlo=")
259                    .unwrap()
260                    .as_slice(),
261            )
262            .unwrap(),
263        };
264
265        assert_eq!(account1, account2);
266    }
267
268    #[test]
269    fn verify_account_phrase() {
270        let account1 = Account {
271            username: "test1".to_string(),
272            api_url: "test1.com".to_string(),
273            private_key: SecretKey::parse_slice(&[
274                234, 169, 139, 200, 30, 42, 176, 229, 16, 101, 229, 85, 125, 47, 182, 24, 154, 8,
275                156, 233, 24, 102, 126, 171, 86, 240, 0, 175, 6, 192, 253, 231,
276            ])
277            .unwrap(),
278        };
279
280        let account2 = Account {
281            username: "test1".to_string(),
282            api_url: "test1.com".to_string(),
283            private_key: Account::phrase_to_private_key([
284                "turkey", "era", "velvet", "detail", "prison", "income", "dose", "royal", "fever",
285                "truly", "unique", "couple", "party", "example", "piece", "art", "leaf", "follow",
286                "rose", "access", "vacant", "gather", "wasp", "audit",
287            ])
288            .unwrap(),
289        };
290
291        assert_eq!(account1, account2)
292    }
293}
294
295#[cfg(test)]
296mod test_account_key_and_phrase {
297    use libsecp256k1::SecretKey;
298    use rand::rngs::OsRng;
299
300    use crate::model::account::Account;
301
302    #[test]
303    fn account_key_and_phrase_eq() {
304        let account1 = Account {
305            username: "test".to_string(),
306            api_url: "test.com".to_string(),
307            private_key: SecretKey::random(&mut OsRng),
308        };
309
310        let phrase = account1.get_phrase().unwrap();
311
312        let reverse = Account::phrase_to_private_key(phrase).unwrap();
313
314        assert!(account1.private_key == reverse);
315    }
316}