mongodb_atlas_cli/secrets/
encoding.rs

1use std::borrow::Cow;
2use std::string::FromUtf8Error;
3
4#[cfg(target_os = "macos")]
5use base64::{DecodeError, Engine, prelude::*};
6use hex::FromHexError;
7use thiserror::Error;
8
9use crate::secrets::SecretStoreError;
10
11#[cfg(target_os = "macos")]
12const HEX_ENCODING_PREFIX: &str = "go-keyring-encoded:";
13#[cfg(target_os = "macos")]
14const BASE64_ENCODING_PREFIX: &str = "go-keyring-base64:";
15
16#[derive(Debug, Error)]
17pub enum DecodePasswordError {
18    #[error("Invalid hex value: {0}")]
19    InvalidHexValue(#[from] FromHexError),
20    #[error("Invalid utf8 value: {0}")]
21    InvalidUtf8Value(#[from] FromUtf8Error),
22    #[cfg(target_os = "macos")]
23    #[error("Invalid base64 value: {0}")]
24    InvalidBase64Value(#[from] DecodeError),
25}
26
27impl From<DecodePasswordError> for SecretStoreError {
28    fn from(error: DecodePasswordError) -> Self {
29        SecretStoreError::Serialization {
30            reason: (error.to_string()),
31        }
32    }
33}
34
35#[cfg(target_os = "macos")]
36pub fn decode_password(password: String) -> Result<Option<String>, DecodePasswordError> {
37    if let Some(hex_encoded_value) = password.strip_prefix(HEX_ENCODING_PREFIX) {
38        let hex = hex::decode(hex_encoded_value)?;
39        let decoded = String::from_utf8(hex)?;
40        return Ok(none_if_empty(decoded));
41    }
42
43    if let Some(base64_encoded_value) = password.strip_prefix(BASE64_ENCODING_PREFIX) {
44        let base64 = BASE64_STANDARD.decode(base64_encoded_value)?;
45        let decoded = String::from_utf8(base64)?;
46        return Ok(none_if_empty(decoded));
47    }
48
49    Ok(none_if_empty(password))
50}
51
52#[cfg(not(target_os = "macos"))]
53pub fn decode_password(password: String) -> Result<Option<String>, DecodePasswordError> {
54    Ok(none_if_empty(password))
55}
56
57fn none_if_empty(password: String) -> Option<String> {
58    if password.is_empty() {
59        None
60    } else {
61        Some(password)
62    }
63}
64
65#[cfg(target_os = "macos")]
66pub fn encode_password<'a>(password: &'a str) -> Cow<'a, str> {
67    // We picked base64 as the default encoding because it is what the zalando keyring library uses by default
68    let base64 = BASE64_STANDARD.encode(password);
69    Cow::Owned(format!("{}{}", BASE64_ENCODING_PREFIX, base64))
70}
71
72#[cfg(not(target_os = "macos"))]
73pub fn encode_password<'a>(password: &'a str) -> Cow<'a, str> {
74    Cow::Borrowed(password)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_decode_hex_password() {
83        let password = "go-keyring-encoded:616263";
84        let decoded = decode_password(password.to_string()).unwrap();
85        assert_eq!(decoded, Some("abc".to_string()));
86    }
87
88    #[test]
89    fn test_decode_base64_password() {
90        let password = "go-keyring-base64:YWJj";
91        let decoded = decode_password(password.to_string()).unwrap();
92        assert_eq!(decoded, Some("abc".to_string()));
93    }
94
95    #[test]
96    fn test_decode_without_prefix() {
97        let password = "abc";
98        let decoded = decode_password(password.to_string()).unwrap();
99        assert_eq!(decoded, Some("abc".to_string()));
100    }
101
102    #[test]
103    fn test_encode_password() {
104        let password = "abc";
105        let encoded = encode_password(password);
106        assert_eq!(encoded, "go-keyring-base64:YWJj");
107    }
108}