Skip to main content

bitwarden_importers/importers/keeper/crypto/
utils.rs

1use rand::Rng;
2
3use super::KeeperCryptoError;
4
5/// Generate `length` cryptographically secure random bytes.
6pub(crate) fn get_random_bytes(length: usize) -> Vec<u8> {
7    let mut buf = vec![0u8; length];
8    bitwarden_random::rng().fill_bytes(&mut buf);
9    buf
10}
11
12/// Encode bytes as unpadded URL-safe base64 (Keeper's `base64UrlEncode`).
13pub(crate) fn base64_url_encode(data: &[u8]) -> String {
14    data_encoding::BASE64URL_NOPAD.encode(data)
15}
16
17/// Decode unpadded URL-safe base64 (Keeper's `base64UrlDecode`).
18///
19/// Any trailing `=` padding is tolerated to match the lenient behaviour of the TypeScript original.
20pub(crate) fn base64_url_decode(text: &str) -> Result<Vec<u8>, KeeperCryptoError> {
21    let trimmed = text.trim_end_matches('=');
22    data_encoding::BASE64URL_NOPAD
23        .decode(trimmed.as_bytes())
24        .map_err(|_| KeeperCryptoError::InvalidData)
25}
26
27#[cfg(test)]
28mod tests {
29    use super::*;
30
31    #[test]
32    fn get_random_bytes_generates_correct_length() {
33        assert_eq!(get_random_bytes(0).len(), 0);
34        assert_eq!(get_random_bytes(1).len(), 1);
35        assert_eq!(get_random_bytes(32).len(), 32);
36        assert_eq!(get_random_bytes(256).len(), 256);
37    }
38
39    #[test]
40    fn get_random_bytes_produces_different_values() {
41        let bytes1 = get_random_bytes(32);
42        let bytes2 = get_random_bytes(32);
43        // Statistically extremely unlikely to generate identical random data
44        assert_ne!(bytes1, bytes2);
45    }
46
47    #[test]
48    fn base64_url_encode_empty() {
49        assert_eq!(base64_url_encode(&[]), "");
50    }
51
52    #[test]
53    fn base64_url_encode_single_byte() {
54        assert_eq!(base64_url_encode(&[0]), "AA");
55        assert_eq!(base64_url_encode(&[255]), "_w");
56    }
57
58    #[test]
59    fn base64_url_encode_uses_url_safe_alphabet() {
60        // Standard base64 would use '+' and '/', URL-safe uses '-' and '_'
61        let data = &[0xfb, 0xff, 0x00];
62        let encoded = base64_url_encode(data);
63        assert!(!encoded.contains('+'));
64        assert!(!encoded.contains('/'));
65        assert!(encoded.contains('-') || encoded.contains('_') || encoded == "u_8A");
66    }
67
68    #[test]
69    fn base64_url_encode_has_no_padding() {
70        // Unpadded base64 should never contain '='
71        let encoded = base64_url_encode(&[0u8; 10]);
72        assert!(!encoded.contains('='));
73    }
74
75    #[test]
76    fn base64_url_decode_empty() {
77        assert_eq!(base64_url_decode("").unwrap(), Vec::<u8>::new());
78    }
79
80    #[test]
81    fn base64_url_decode_single_byte() {
82        assert_eq!(base64_url_decode("AA").unwrap(), vec![0]);
83        assert_eq!(base64_url_decode("_w").unwrap(), vec![255]);
84    }
85
86    #[test]
87    fn base64_url_decode_tolerates_padding() {
88        // Should accept trailing '=' even though we generate unpadded
89        assert_eq!(base64_url_decode("AA").unwrap(), vec![0]);
90        assert_eq!(base64_url_decode("AA=").unwrap(), vec![0]);
91        assert_eq!(base64_url_decode("AA==").unwrap(), vec![0]);
92    }
93
94    #[test]
95    fn base64_url_decode_rejects_invalid_input() {
96        assert!(base64_url_decode("!!!").is_err());
97        assert!(base64_url_decode("@@@@").is_err());
98        assert!(base64_url_decode("\x00\x01\x02").is_err());
99    }
100
101    #[test]
102    fn base64_url_round_trip() {
103        let original = b"Hello, Keeper!";
104        let encoded = base64_url_encode(original);
105        let decoded = base64_url_decode(&encoded).unwrap();
106        assert_eq!(decoded, original);
107    }
108
109    #[test]
110    fn base64_url_round_trip_binary() {
111        let original = [0xfb, 0xff, 0x00, 0x10, 0x3e, 0x7d];
112        let encoded = base64_url_encode(&original);
113        let decoded = base64_url_decode(&encoded).unwrap();
114        assert_eq!(decoded.as_slice(), &original);
115    }
116
117    #[test]
118    fn base64_url_round_trip_empty() {
119        let original: &[u8] = &[];
120        let encoded = base64_url_encode(original);
121        let decoded: Vec<u8> = base64_url_decode(&encoded).unwrap();
122        assert_eq!(decoded.as_slice(), original);
123    }
124
125    #[test]
126    fn base64_url_round_trip_various_lengths() {
127        for length in [0, 1, 2, 3, 4, 5, 10, 31, 32, 33, 64, 255] {
128            let original = get_random_bytes(length);
129            let encoded = base64_url_encode(&original);
130            let decoded = base64_url_decode(&encoded).unwrap();
131            assert_eq!(decoded, original, "Round trip failed for length {}", length);
132        }
133    }
134
135    #[test]
136    fn base64_url_encode_known_vectors() {
137        // Test against known base64url vectors (RFC 4648)
138        assert_eq!(base64_url_encode(b"abc"), "YWJj");
139        assert_eq!(base64_url_encode(b"abcd"), "YWJjZA");
140        // The following contains standard base64 padding, but we use unpadded
141        assert_eq!(base64_url_encode(b"ab"), "YWI");
142    }
143
144    #[test]
145    fn base64_url_decode_known_vectors() {
146        assert_eq!(base64_url_decode("YWJj").unwrap(), b"abc");
147        assert_eq!(base64_url_decode("YWJjZA").unwrap(), b"abcd");
148        assert_eq!(base64_url_decode("YWI").unwrap(), b"ab");
149    }
150}