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