1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
910use std::error::Error;
11use std::fmt;
12use std::str::FromStr;
1314use base64::Engine;
15use base64::engine::general_purpose::URL_SAFE_NO_PAD;
16use base64::{
17 alphabet,
18 engine::{GeneralPurpose, GeneralPurposeConfig},
19};
20use serde::Deserialize;
21use uuid::Uuid;
2223/// The prefix that identifies an app password as a Materialize password.
24pub const PREFIX: &str = "mzp_";
2526/// A Materialize app password.
27///
28/// Somewhat unusually, the app password encodes both the client ID and secret
29/// for the API key in use. Both the client ID and secret are UUIDs. The
30/// password can have one of two formats:
31///
32/// * The URL-safe base64 encoding of the concatenated bytes of the UUIDs.
33///
34/// This format is a very compact representation (only 43 or 44 bytes)
35/// that is safe to use in a connection string without escaping.
36///
37/// * The concatenated hex-encoding of the UUIDs, with any number of
38/// special characters that are ignored.
39///
40/// This format allows for the UUIDs to be formatted with hyphens, or
41/// not.
42///
43#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct AppPassword {
45/// The client ID embedded in the app password.
46pub client_id: Uuid,
47/// The secret key embedded in the app password.
48pub secret_key: Uuid,
49}
5051impl fmt::Display for AppPassword {
52fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
53let mut buf = vec![];
54 buf.extend(self.client_id.as_bytes());
55 buf.extend(self.secret_key.as_bytes());
56let encoded = URL_SAFE_NO_PAD.encode(buf);
57 f.write_str(PREFIX)?;
58 f.write_str(&encoded)
59 }
60}
6162impl FromStr for AppPassword {
63type Err = AppPasswordParseError;
6465fn from_str(password: &str) -> Result<AppPassword, AppPasswordParseError> {
66let password = password.strip_prefix(PREFIX).ok_or(AppPasswordParseError)?;
67if password.len() == 43 || password.len() == 44 {
68// If it's exactly 43 or 44 bytes, assume we have base64-encoded
69 // UUID bytes without or with padding, respectively.
70let url_safe_engine = GeneralPurpose::new(
71&alphabet::URL_SAFE,
72 GeneralPurposeConfig::new()
73 .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
74 );
75let buf = url_safe_engine
76 .decode(password)
77 .map_err(|_| AppPasswordParseError)?;
78let client_id = Uuid::from_slice(&buf[..16]).map_err(|_| AppPasswordParseError)?;
79let secret_key = Uuid::from_slice(&buf[16..]).map_err(|_| AppPasswordParseError)?;
80Ok(AppPassword {
81 client_id,
82 secret_key,
83 })
84 } else if password.len() >= 64 {
85// If it's more than 64 bytes, assume we have concatenated
86 // hex-encoded UUIDs, possibly with some special characters mixed
87 // in.
88let mut chars = password.chars().filter(|c| c.is_alphanumeric());
89let client_id = Uuid::parse_str(&chars.by_ref().take(32).collect::<String>())
90 .map_err(|_| AppPasswordParseError)?;
91let secret_key = Uuid::parse_str(&chars.take(32).collect::<String>())
92 .map_err(|_| AppPasswordParseError)?;
93Ok(AppPassword {
94 client_id,
95 secret_key,
96 })
97 } else {
98// Otherwise it's definitely not a password format we understand.
99Err(AppPasswordParseError)
100 }
101 }
102}
103104/// An error while parsing an [`AppPassword`].
105#[derive(Clone, Debug)]
106pub struct AppPasswordParseError;
107108impl Error for AppPasswordParseError {}
109110impl fmt::Display for AppPasswordParseError {
111fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112 f.write_str("invalid app password format")
113 }
114}
115116#[cfg(test)]
117mod tests {
118use uuid::Uuid;
119120use super::AppPassword;
121122#[mz_ore::test]
123fn test_app_password() {
124struct TestCase {
125 input: &'static str,
126 expected_output: &'static str,
127 expected_client_id: Uuid,
128 expected_secret_key: Uuid,
129 }
130131for tc in [
132 TestCase {
133 input: "mzp_7ce3c1e8ea854594ad5d785f17d1736f1947fdcef5404adb84a47347e5d30c9f",
134 expected_output: "mzp_fOPB6OqFRZStXXhfF9FzbxlH_c71QErbhKRzR-XTDJ8",
135 expected_client_id: "7ce3c1e8-ea85-4594-ad5d-785f17d1736f".parse().unwrap(),
136 expected_secret_key: "1947fdce-f540-4adb-84a4-7347e5d30c9f".parse().unwrap(),
137 },
138 TestCase {
139 input: "mzp_fOPB6OqFRZStXXhfF9FzbxlH_c71QErbhKRzR-XTDJ8",
140 expected_output: "mzp_fOPB6OqFRZStXXhfF9FzbxlH_c71QErbhKRzR-XTDJ8",
141 expected_client_id: "7ce3c1e8-ea85-4594-ad5d-785f17d1736f".parse().unwrap(),
142 expected_secret_key: "1947fdce-f540-4adb-84a4-7347e5d30c9f".parse().unwrap(),
143 },
144 TestCase {
145 input: "mzp_0445db36-5826-41af-84f6-e09402fc6171:a0c11434-07ba-426a-b83d-cc4f192325a3",
146 expected_output: "mzp_BEXbNlgmQa-E9uCUAvxhcaDBFDQHukJquD3MTxkjJaM",
147 expected_client_id: "0445db36-5826-41af-84f6-e09402fc6171".parse().unwrap(),
148 expected_secret_key: "a0c11434-07ba-426a-b83d-cc4f192325a3".parse().unwrap(),
149 },
150 ] {
151let app_password: AppPassword = tc.input.parse().unwrap();
152assert_eq!(app_password.to_string(), tc.expected_output);
153assert_eq!(app_password.client_id, tc.expected_client_id);
154assert_eq!(app_password.secret_key, tc.expected_secret_key);
155 }
156 }
157}