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