Skip to main content

bitwarden_encoding/
b64.rs

1use std::str::FromStr;
2
3use data_encoding::BASE64;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::{FromStrVisitor, chunked::chunked_encode};
8
9/// Base64 encoded data
10///
11/// Is indifferent about padding when decoding, but always produces padding when encoding.
12#[cfg(feature = "wasm")]
13#[derive(Debug, Serialize, Clone, Hash, PartialEq, Eq)]
14#[serde(into = "String")]
15#[bitwarden_ffi::wasm_record]
16pub struct B64(#[tsify(type = "string")] Vec<u8>);
17
18/// Base64 encoded data
19///
20/// Is indifferent about padding when decoding, but always produces padding when encoding.
21#[cfg(not(feature = "wasm"))]
22#[derive(Debug, Serialize, Clone, Hash, PartialEq, Eq)]
23#[serde(into = "String")]
24pub struct B64(Vec<u8>);
25
26impl B64 {
27    /// Returns a byte slice of the inner vector.
28    pub fn as_bytes(&self) -> &[u8] {
29        &self.0
30    }
31
32    /// Returns the inner byte vector.
33    pub fn into_bytes(self) -> Vec<u8> {
34        self.0
35    }
36}
37
38// We manually implement this to handle both `String` and `&str`
39impl<'de> Deserialize<'de> for B64 {
40    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41    where
42        D: serde::Deserializer<'de>,
43    {
44        deserializer.deserialize_str(FromStrVisitor::new())
45    }
46}
47
48impl From<Vec<u8>> for B64 {
49    fn from(src: Vec<u8>) -> Self {
50        Self(src)
51    }
52}
53impl From<&[u8]> for B64 {
54    fn from(src: &[u8]) -> Self {
55        Self(src.to_vec())
56    }
57}
58
59impl From<B64> for Vec<u8> {
60    fn from(src: B64) -> Self {
61        src.0
62    }
63}
64
65impl From<B64> for String {
66    fn from(src: B64) -> Self {
67        String::from(&src)
68    }
69}
70
71impl From<&B64> for String {
72    fn from(src: &B64) -> Self {
73        chunked_encode(&BASE64, &src.0)
74    }
75}
76
77impl std::fmt::Display for B64 {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.write_str(String::from(self).as_str())
80    }
81}
82
83/// An error returned when a string is not base64 decodable.
84#[derive(Debug, Error)]
85#[error("Data isn't base64 encoded")]
86pub struct NotB64EncodedError;
87
88const BASE64_PERMISSIVE: data_encoding::Encoding = data_encoding_macro::new_encoding! {
89    symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
90    padding: None,
91    check_trailing_bits: false,
92};
93const BASE64_PADDING: &str = "=";
94
95impl TryFrom<String> for B64 {
96    type Error = NotB64EncodedError;
97
98    fn try_from(value: String) -> Result<Self, Self::Error> {
99        Self::try_from(value.as_str())
100    }
101}
102
103impl TryFrom<&str> for B64 {
104    type Error = NotB64EncodedError;
105
106    fn try_from(value: &str) -> Result<Self, Self::Error> {
107        let sane_string = value.trim_end_matches(BASE64_PADDING);
108        BASE64_PERMISSIVE
109            .decode(sane_string.as_bytes())
110            .map(Self)
111            .map_err(|_| NotB64EncodedError)
112    }
113}
114
115impl FromStr for B64 {
116    type Err = NotB64EncodedError;
117
118    fn from_str(s: &str) -> Result<Self, Self::Err> {
119        Self::try_from(s)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_b64_from_vec() {
129        let data = vec![72, 101, 108, 108, 111];
130        let b64 = B64::from(data.clone());
131        assert_eq!(Vec::<u8>::from(b64), data);
132    }
133
134    #[test]
135    fn test_b64_from_slice() {
136        let data = b"Hello";
137        let b64 = B64::from(data.as_slice());
138        assert_eq!(b64.as_bytes(), data);
139    }
140
141    #[test]
142    fn test_b64_encoding_with_padding() {
143        let data = b"Hello, World!";
144        let b64 = B64::from(data.as_slice());
145        let encoded = String::from(&b64);
146        assert_eq!(encoded, "SGVsbG8sIFdvcmxkIQ==");
147        assert!(encoded.contains('='));
148    }
149
150    #[test]
151    fn test_b64_decoding_with_padding() {
152        let encoded_with_padding = "SGVsbG8sIFdvcmxkIQ==";
153        let b64 = B64::try_from(encoded_with_padding).unwrap();
154        assert_eq!(b64.as_bytes(), b"Hello, World!");
155    }
156
157    #[test]
158    fn test_b64_decoding_without_padding() {
159        let encoded_without_padding = "SGVsbG8sIFdvcmxkIQ";
160        let b64 = B64::try_from(encoded_without_padding).unwrap();
161        assert_eq!(b64.as_bytes(), b"Hello, World!");
162    }
163
164    #[test]
165    fn test_b64_round_trip_with_padding() {
166        let original = b"Test data that requires padding!";
167        let b64 = B64::from(original.as_slice());
168        let encoded = String::from(&b64);
169        let decoded = B64::try_from(encoded.as_str()).unwrap();
170        assert_eq!(decoded.as_bytes(), original);
171    }
172
173    #[test]
174    fn test_b64_round_trip_without_padding() {
175        let original = b"Test data";
176        let b64 = B64::from(original.as_slice());
177        let encoded = String::from(&b64);
178        let decoded = B64::try_from(encoded.as_str()).unwrap();
179        assert_eq!(decoded.as_bytes(), original);
180    }
181
182    #[test]
183    fn test_b64_display() {
184        let data = b"Hello";
185        let b64 = B64::from(data.as_slice());
186        assert_eq!(b64.to_string(), "SGVsbG8=");
187    }
188
189    #[test]
190    fn test_b64_invalid_encoding() {
191        let invalid_b64 = "This is not base64!@#$";
192        let result = B64::try_from(invalid_b64);
193        assert!(result.is_err());
194    }
195
196    #[test]
197    fn test_b64_empty_string() {
198        let empty = "";
199        let b64 = B64::try_from(empty).unwrap();
200        assert_eq!(b64.as_bytes().len(), 0);
201    }
202
203    #[test]
204    fn test_b64_padding_removal() {
205        let encoded_with_padding = "SGVsbG8sIFdvcmxkIQ==";
206        let b64 = B64::try_from(encoded_with_padding).unwrap();
207        assert_eq!(b64.as_bytes(), b"Hello, World!");
208    }
209
210    #[test]
211    fn test_b64_serialization() {
212        let data = b"serialization test";
213        let b64 = B64::from(data.as_slice());
214
215        let serialized = serde_json::to_string(&b64).unwrap();
216        assert_eq!(serialized, "\"c2VyaWFsaXphdGlvbiB0ZXN0\"");
217
218        let deserialized: B64 = serde_json::from_str(&serialized).unwrap();
219        assert_eq!(b64.as_bytes(), deserialized.as_bytes());
220    }
221
222    #[test]
223    fn test_not_b64_encoded_error_display() {
224        let error = NotB64EncodedError;
225        assert_eq!(error.to_string(), "Data isn't base64 encoded");
226    }
227
228    #[test]
229    fn test_b64_from_str() {
230        let encoded = "SGVsbG8sIFdvcmxkIQ==";
231        let b64: B64 = encoded.parse().unwrap();
232        assert_eq!(b64.as_bytes(), b"Hello, World!");
233    }
234
235    #[test]
236    fn test_b64_eq_and_hash() {
237        let data1 = b"test data";
238        let data2 = b"test data";
239        let data3 = b"different data";
240
241        let b64_1 = B64::from(data1.as_slice());
242        let b64_2 = B64::from(data2.as_slice());
243        let b64_3 = B64::from(data3.as_slice());
244
245        assert_eq!(b64_1, b64_2);
246        assert_ne!(b64_1, b64_3);
247
248        use std::{
249            collections::hash_map::DefaultHasher,
250            hash::{Hash, Hasher},
251        };
252
253        let mut hasher1 = DefaultHasher::new();
254        let mut hasher2 = DefaultHasher::new();
255
256        b64_1.hash(&mut hasher1);
257        b64_2.hash(&mut hasher2);
258
259        assert_eq!(hasher1.finish(), hasher2.finish());
260    }
261}