wallet_dat/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7//!
8//! The wallet.dat structure is a binary file containing the seed we get from a bip39 mnemonic,
9//! in encrypted form via a password
10#![no_std]
11extern crate alloc;
12
13use constants::*;
14use crypto::{decrypt, encrypt};
15use error::{Result, WalletDataFileError as Error};
16
17use seed::Seed;
18
19use alloc::vec::Vec;
20
21mod crypto;
22mod error;
23mod seed;
24mod constants {
25    /// Binary prefix for new binary file format
26    pub const MAGIC: u32 = 0x72736b;
27    /// Binary prefix for old Dusk wallet files
28    pub const OLD_MAGIC: u32 = 0x1d0c15;
29    pub const FILE_TYPE: u16 = 0x0200;
30    pub const RESERVED: u16 = 0x0000;
31    pub const LATEST_VERSION: super::Version = (0, 0, 2, 0, false);
32}
33
34type Version = (u8, u8, u8, u8, bool);
35
36/// Versions of the potential wallet DAT files we read
37#[derive(Copy, Clone, Debug, PartialEq)]
38pub enum DatFileVersion {
39    /// Legacy the oldest format
40    Legacy,
41    /// Preciding legacy, we have the old one
42    OldWalletCli(Version),
43    /// The newest one. All new saves are saved in this file format
44    RuskBinaryFileFormat(Version),
45}
46
47impl DatFileVersion {
48    /// Checks if the file version is older than the latest Rust Binary file
49    /// format
50    pub fn is_old(&self) -> bool {
51        matches!(self, Self::Legacy | Self::OldWalletCli(_))
52    }
53}
54
55/// Make sense of the payload and return it
56pub fn get_seed_and_address(
57    file: DatFileVersion,
58    mut bytes: Vec<u8>,
59    pwd: &str,
60) -> Result<(Seed, u64)> {
61    let pwd = blake3::hash(pwd.as_bytes());
62    let pwd = pwd.as_bytes();
63
64    match file {
65        DatFileVersion::Legacy => {
66            if bytes[1] == 0 && bytes[2] == 0 {
67                bytes.drain(..3);
68            }
69
70            bytes = decrypt(&bytes, pwd)?;
71
72            // get our seed
73            let seed = Seed::from_reader(&bytes[..]).map_err(|_| Error::WalletFileCorrupted)?;
74
75            Ok((seed, 1))
76        }
77        DatFileVersion::OldWalletCli((major, minor, _, _, _)) => {
78            bytes.drain(..5);
79
80            let result: Result<(Seed, _)> = match (major, minor) {
81                (1, 0) => {
82                    let content = decrypt(&bytes, pwd)?;
83                    let buff = &content[..];
84
85                    let seed = Seed::from_reader(buff).map_err(|_| Error::WalletFileCorrupted)?;
86
87                    Ok((seed, 0))
88                }
89                (2, 0) => {
90                    let content = decrypt(&bytes, pwd)?;
91                    let buff = &content[..];
92
93                    // extract seed
94                    let seed = Seed::from_reader(buff).map_err(|_| Error::WalletFileCorrupted)?;
95
96                    // extract addresses count
97                    Ok((seed, 0))
98                }
99                _ => Err(Error::UnknownFileVersion(major, minor)),
100            };
101
102            result
103        }
104        DatFileVersion::RuskBinaryFileFormat(version) => {
105            let rest = bytes.get(12..(12 + 96));
106
107            if let Some(rest) = rest {
108                let content = decrypt(rest, pwd)?;
109
110                if let Some(seed_buff) = content.get(0..65) {
111                    // first 64 bytes are the seed
112                    let seed = Seed::from_reader(&seed_buff[0..64])
113                        .map_err(|_| Error::WalletFileCorrupted)?;
114
115                    match version {
116                        (0, 0, 2, 0, false) => {
117                            if let Some(last_pos_bytes) = content.get(64..72) {
118                                let last_pos = match last_pos_bytes.try_into() {
119                                    Ok(last_pos_bytes) => u64::from_le_bytes(last_pos_bytes),
120                                    Err(_) => return Err(Error::NoLastPosFound),
121                                };
122
123                                Ok((seed, last_pos))
124                            } else {
125                                Err(Error::WalletFileCorrupted)
126                            }
127                        }
128                        _ => Ok((seed, 0)),
129                    }
130                } else {
131                    Err(Error::WalletFileCorrupted)
132                }
133            } else {
134                Err(Error::WalletFileCorrupted)
135            }
136        }
137    }
138}
139
140/// From the first 12 bytes of the file (header), we check version
141///
142/// https://github.com/dusk-network/rusk/wiki/Binary-File-Format/#header
143pub fn check_version(bytes: Option<&[u8]>) -> Result<DatFileVersion> {
144    match bytes {
145        Some(bytes) => {
146            let header_bytes: [u8; 4] = bytes[0..4]
147                .try_into()
148                .map_err(|_| Error::WalletFileCorrupted)?;
149
150            let magic = u32::from_le_bytes(header_bytes) & 0x00ffffff;
151
152            if magic == OLD_MAGIC {
153                // check for version information
154                let (major, minor) = (bytes[3], bytes[4]);
155
156                Ok(DatFileVersion::OldWalletCli((major, minor, 0, 0, false)))
157            } else {
158                let header_bytes = bytes[0..8]
159                    .try_into()
160                    .map_err(|_| Error::WalletFileCorrupted)?;
161
162                let number = u64::from_be_bytes(header_bytes);
163
164                let magic_num = (number & 0xFFFFFF00000000) >> 32;
165
166                if (magic_num as u32) != MAGIC {
167                    return Ok(DatFileVersion::Legacy);
168                }
169
170                let file_type = (number & 0x000000FFFF0000) >> 16;
171                let reserved = number & 0x0000000000FFFF;
172
173                if file_type != FILE_TYPE as u64 {
174                    return Err(Error::WalletFileCorrupted);
175                };
176
177                if reserved != RESERVED as u64 {
178                    return Err(Error::WalletFileCorrupted);
179                };
180
181                let version_bytes = bytes[8..12]
182                    .try_into()
183                    .map_err(|_| Error::WalletFileCorrupted)?;
184
185                let version = u32::from_be_bytes(version_bytes);
186
187                let major = (version & 0xff000000) >> 24;
188                let minor = (version & 0x00ff0000) >> 16;
189                let patch = (version & 0x0000ff00) >> 8;
190                let pre = (version & 0x000000f0) >> 4;
191                let higher = version & 0x0000000f;
192
193                let pre_higher = matches!(higher, 1);
194
195                Ok(DatFileVersion::RuskBinaryFileFormat((
196                    major as u8,
197                    minor as u8,
198                    patch as u8,
199                    pre as u8,
200                    pre_higher,
201                )))
202            }
203        }
204        None => Err(Error::WalletFileCorrupted),
205    }
206}
207
208pub fn encrypt_seed(seed: &[u8; 64], pwd: &str, last_pos: u64) -> Result<Vec<u8>> {
209    let mut header = Vec::with_capacity(12);
210    header.extend_from_slice(&MAGIC.to_be_bytes());
211    // File type = Rusk Wallet (0x02)
212    header.extend_from_slice(&FILE_TYPE.to_be_bytes());
213    // Reserved (0x0)
214    header.extend_from_slice(&RESERVED.to_be_bytes());
215    // Version
216    header.extend_from_slice(&version_bytes(LATEST_VERSION));
217
218    let mut payload = Vec::from(seed);
219
220    payload.extend(last_pos.to_le_bytes());
221
222    let pwd = blake3::hash(pwd.as_bytes());
223    let pwd = pwd.as_bytes();
224
225    // encrypt the payload
226    payload = encrypt(&payload, pwd)?;
227
228    let mut content = Vec::with_capacity(header.len() + payload.len());
229
230    content.extend_from_slice(&header);
231    content.extend_from_slice(&payload);
232
233    Ok(content)
234}
235
236pub(crate) fn version_bytes(version: Version) -> [u8; 4] {
237    u32::from_be_bytes([version.0, version.1, version.2, version.3]).to_be_bytes()
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use alloc::vec::Vec;
244
245    #[test]
246    fn distiction_between_versions() {
247        // with magic number
248        let old_wallet_file = Vec::from([0x15, 0x0c, 0x1d, 0x02, 0x00]);
249        // no magic number just nonsense bytes
250        let legacy_file = Vec::from([
251            0xab, 0x38, 0x81, 0x3b, 0xfc, 0x79, 0x11, 0xf9, 0x86, 0xd6, 0xd0,
252        ]);
253        // new header
254        let new_file = Vec::from([
255            0x00, 0x72, 0x73, 0x6b, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
256        ]);
257
258        assert_eq!(
259            check_version(Some(&old_wallet_file)).unwrap(),
260            DatFileVersion::OldWalletCli((2, 0, 0, 0, false))
261        );
262
263        assert_eq!(
264            check_version(Some(&legacy_file)).unwrap(),
265            DatFileVersion::Legacy
266        );
267
268        assert_eq!(
269            check_version(Some(&new_file)).unwrap(),
270            DatFileVersion::RuskBinaryFileFormat((0, 0, 1, 0, false))
271        );
272    }
273
274    #[test]
275    fn generate_latest_version() {
276        let seed: [u8; 64] = [0; 64];
277        let encryped = encrypt_seed(&seed, "password", 304).unwrap();
278
279        let version = check_version(Some(&encryped)).unwrap();
280
281        assert_eq!(
282            version,
283            DatFileVersion::RuskBinaryFileFormat(LATEST_VERSION)
284        );
285
286        let (returned_seed, last_pos) =
287            get_seed_and_address(version, encryped, "password").unwrap();
288
289        assert_eq!(seed, returned_seed.as_bytes());
290
291        assert_eq!(last_pos, 304);
292    }
293}