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, "{byte:08b}");
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, "{byte:08b}");
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!("{byte:08b}"));
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, Deserializer};
161    use serde::ser::Serializer;
162
163    pub fn serialize<S>(sk: &SecretKey, serializer: S) -> Result<S::Ok, S::Error>
164    where
165        S: Serializer,
166    {
167        serializer.serialize_bytes(&sk.serialize())
168    }
169
170    pub fn deserialize<'de, D>(deserializer: D) -> Result<SecretKey, D::Error>
171    where
172        D: Deserializer<'de>,
173    {
174        let key = <Vec<u8>>::deserialize(deserializer)?;
175        let sk = SecretKey::parse_slice(&key).map_err(serde::de::Error::custom)?;
176        Ok(sk)
177    }
178}
179
180#[cfg(test)]
181mod test_account_serialization {
182    use libsecp256k1::SecretKey;
183    use rand::rngs::OsRng;
184
185    use crate::model::account::Account;
186
187    #[test]
188    fn account_serialize_deserialize() {
189        let account1 = Account {
190            username: "test".to_string(),
191            api_url: "test.com".to_string(),
192            private_key: SecretKey::random(&mut OsRng),
193        };
194
195        let encoded: Vec<u8> = bincode::serialize(&account1).unwrap();
196        let account2: Account = bincode::deserialize(&encoded).unwrap();
197
198        assert_eq!(account1, account2);
199    }
200
201    #[test]
202    fn verify_account_v1() {
203        let account1 = Account {
204            username: "test1".to_string(),
205            api_url: "test1.com".to_string(),
206            private_key: SecretKey::parse_slice(&[
207                19, 34, 85, 4, 36, 83, 52, 122, 49, 107, 223, 44, 31, 16, 2, 160, 100, 103, 193, 0,
208                67, 15, 184, 133, 33, 111, 91, 143, 137, 232, 240, 42,
209            ])
210            .unwrap(),
211        };
212
213        let account2 = bincode::deserialize(
214            base64::decode("BQAAAAAAAAB0ZXN0MQkAAAAAAAAAdGVzdDEuY29tIAAAAAAAAAATIlUEJFM0ejFr3ywfEAKgZGfBAEMPuIUhb1uPiejwKg==")
215                .unwrap()
216                .as_slice()
217        ).unwrap();
218
219        assert_eq!(account1, account2);
220    }
221
222    #[test]
223    fn verify_account_v2() {
224        let account1 = Account {
225            username: "test1".to_string(),
226            api_url: "test1.com".to_string(),
227            private_key: SecretKey::parse_slice(&[
228                158, 250, 59, 72, 139, 112, 93, 137, 168, 199, 28, 230, 56, 37, 43, 52, 152, 176,
229                243, 149, 124, 11, 2, 126, 73, 118, 252, 112, 225, 207, 34, 90,
230            ])
231            .unwrap(),
232        };
233
234        let account2 = Account {
235            username: "test1".to_string(),
236            api_url: "test1.com".to_string(),
237            private_key: SecretKey::parse_slice(
238                base64::decode("nvo7SItwXYmoxxzmOCUrNJiw85V8CwJ+SXb8cOHPIlo=")
239                    .unwrap()
240                    .as_slice(),
241            )
242            .unwrap(),
243        };
244
245        assert_eq!(account1, account2);
246    }
247
248    #[test]
249    fn verify_account_phrase() {
250        let account1 = Account {
251            username: "test1".to_string(),
252            api_url: "test1.com".to_string(),
253            private_key: SecretKey::parse_slice(&[
254                234, 169, 139, 200, 30, 42, 176, 229, 16, 101, 229, 85, 125, 47, 182, 24, 154, 8,
255                156, 233, 24, 102, 126, 171, 86, 240, 0, 175, 6, 192, 253, 231,
256            ])
257            .unwrap(),
258        };
259
260        let account2 = Account {
261            username: "test1".to_string(),
262            api_url: "test1.com".to_string(),
263            private_key: Account::phrase_to_private_key([
264                "turkey", "era", "velvet", "detail", "prison", "income", "dose", "royal", "fever",
265                "truly", "unique", "couple", "party", "example", "piece", "art", "leaf", "follow",
266                "rose", "access", "vacant", "gather", "wasp", "audit",
267            ])
268            .unwrap(),
269        };
270
271        assert_eq!(account1, account2)
272    }
273}
274
275#[cfg(test)]
276mod test_account_key_and_phrase {
277    use libsecp256k1::SecretKey;
278    use rand::rngs::OsRng;
279
280    use crate::model::account::Account;
281
282    #[test]
283    fn account_key_and_phrase_eq() {
284        let account1 = Account {
285            username: "test".to_string(),
286            api_url: "test.com".to_string(),
287            private_key: SecretKey::random(&mut OsRng),
288        };
289
290        let phrase = account1.get_phrase().unwrap();
291
292        let reverse = Account::phrase_to_private_key(phrase).unwrap();
293
294        assert!(account1.private_key == reverse);
295    }
296}