Skip to main content

bitwarden_crypto/keys/
key_id.rs

1use std::str::FromStr;
2
3use bitwarden_encoding::FromStrVisitor;
4use rand::RngExt;
5use serde::{Deserialize, Serialize};
6use subtle::ConstantTimeEq;
7#[cfg(feature = "wasm")]
8use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
9use zeroize::Zeroize;
10
11use crate::{CryptoError, error::EncodingError};
12
13#[cfg(feature = "wasm")]
14#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
15const TS_CUSTOM_TYPES: &'static str = r#"
16export type KeyId = Tagged<string, "KeyId">;
17"#;
18
19/// Since `KeyId` is a wrapper around UUIDs, this is statically 16 bytes.
20pub(crate) const KEY_ID_SIZE: usize = 16;
21
22/// A key id is a unique identifier for a single key. There is a 1:1 mapping between key ID and key
23/// bytes, so something like a user key rotation is replacing the key with ID A with a new key with
24/// ID B.
25#[derive(Clone, PartialEq, Zeroize)]
26pub struct KeyId([u8; KEY_ID_SIZE]);
27
28// Constant time here is not implemented because the key-id itself is secret, it is not.
29// Instead, it is implemented to correctly allow other things that implement ct_eq to correctly
30// implement the ct_eq contract. This is the case for COSE keys that have key material and a key id.
31impl ConstantTimeEq for KeyId {
32    fn ct_eq(&self, other: &Self) -> subtle::Choice {
33        self.0.ct_eq(&other.0)
34    }
35}
36
37/// Fixed length identifiers for keys.
38/// These are intended to be unique and constant per-key.
39///
40/// Currently these are randomly generated 16 byte identifiers, which is considered safe to randomly
41/// generate with vanishingly small collision chance. However, the generation of IDs is an internal
42/// concern and may change in the future.
43impl KeyId {
44    /// Creates a new random key ID randomly, sampled from the crates CSPRNG.
45    pub fn make() -> Self {
46        let mut rng = bitwarden_random::rng();
47        let mut key_id = [0u8; KEY_ID_SIZE];
48        rng.fill(&mut key_id);
49        Self(key_id)
50    }
51
52    /// Returns the key ID as a slice of bytes.
53    pub fn as_slice(&self) -> &[u8] {
54        &self.0
55    }
56}
57
58impl TryFrom<&[u8]> for KeyId {
59    type Error = &'static str;
60
61    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
62        if value.len() != KEY_ID_SIZE {
63            return Err("Invalid length for KeyId");
64        }
65        let mut key_id = [0u8; KEY_ID_SIZE];
66        key_id.copy_from_slice(value);
67        Ok(Self(key_id))
68    }
69}
70
71impl From<KeyId> for [u8; KEY_ID_SIZE] {
72    fn from(key_id: KeyId) -> Self {
73        key_id.0
74    }
75}
76
77impl From<&KeyId> for Vec<u8> {
78    fn from(key_id: &KeyId) -> Self {
79        key_id.0.as_slice().to_vec()
80    }
81}
82
83impl From<[u8; KEY_ID_SIZE]> for KeyId {
84    fn from(bytes: [u8; KEY_ID_SIZE]) -> Self {
85        Self(bytes)
86    }
87}
88
89/// Key ids travel on the wire as a lowercase hex encoding of the 16 raw bytes, giving a fixed
90/// 32-character string. The server rejects any other encoding.
91impl std::fmt::Display for KeyId {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{}", hex::encode(self.0))
94    }
95}
96
97/// Parses the hex encoding produced by [`Display`](std::fmt::Display). Anything that is not exactly
98/// 16 bytes worth of hex is rejected.
99impl FromStr for KeyId {
100    type Err = CryptoError;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        let bytes = hex::decode(s).map_err(|_| EncodingError::InvalidValue("key id"))?;
104        Self::try_from(bytes.as_slice()).map_err(|_| EncodingError::InvalidValue("key id").into())
105    }
106}
107
108impl Serialize for KeyId {
109    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
110    where
111        S: serde::Serializer,
112    {
113        serializer.serialize_str(&self.to_string())
114    }
115}
116
117impl<'de> Deserialize<'de> for KeyId {
118    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
119    where
120        D: serde::Deserializer<'de>,
121    {
122        deserializer.deserialize_str(FromStrVisitor::new())
123    }
124}
125
126// Key ids cross the WASM boundary as their hex string form, mirroring the other crypto types.
127#[cfg(feature = "wasm")]
128impl wasm_bindgen::describe::WasmDescribe for KeyId {
129    fn describe() {
130        <String as wasm_bindgen::describe::WasmDescribe>::describe();
131    }
132}
133
134#[cfg(feature = "wasm")]
135impl FromWasmAbi for KeyId {
136    type Abi = <String as FromWasmAbi>::Abi;
137
138    unsafe fn from_abi(abi: Self::Abi) -> Self {
139        use wasm_bindgen::UnwrapThrowExt;
140
141        let s = unsafe { String::from_abi(abi) };
142        Self::from_str(&s).unwrap_throw()
143    }
144}
145
146#[cfg(feature = "wasm")]
147impl OptionFromWasmAbi for KeyId {
148    fn is_none(abi: &Self::Abi) -> bool {
149        <String as OptionFromWasmAbi>::is_none(abi)
150    }
151}
152
153#[cfg(feature = "wasm")]
154impl IntoWasmAbi for KeyId {
155    type Abi = <String as IntoWasmAbi>::Abi;
156
157    fn into_abi(self) -> Self::Abi {
158        self.to_string().into_abi()
159    }
160}
161
162#[cfg(feature = "wasm")]
163impl TryFrom<wasm_bindgen::JsValue> for KeyId {
164    type Error = CryptoError;
165
166    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
167        let string = value
168            .as_string()
169            .ok_or(EncodingError::InvalidValue("key id"))?;
170        Self::from_str(&string)
171    }
172}
173
174impl std::fmt::Debug for KeyId {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        write!(f, "KeyId({})", hex::encode(self.0))
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    #[ignore = "Manual test to verify debug format"]
186    fn test_key_id_debug() {
187        let key_id = KeyId::make();
188        println!("{:?}", key_id);
189    }
190
191    const TEST_KEY_ID_HEX: &str = "000102030405060708090a0b0c0d0e0f";
192
193    fn test_key_id() -> KeyId {
194        KeyId::from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
195    }
196
197    #[test]
198    fn test_from_str_roundtrips_display() {
199        let key_id = KeyId::make();
200
201        assert_eq!(KeyId::from_str(&key_id.to_string()).unwrap(), key_id);
202    }
203
204    #[test]
205    fn test_from_str_parses_lowercase_hex() {
206        assert_eq!(KeyId::from_str(TEST_KEY_ID_HEX).unwrap(), test_key_id());
207    }
208
209    #[test]
210    fn test_from_str_rejects_non_hex() {
211        assert!(KeyId::from_str("not a key id at all, no sir").is_err());
212    }
213
214    #[test]
215    fn test_from_str_rejects_odd_length() {
216        assert!(KeyId::from_str("000102030405060708090a0b0c0d0e0").is_err());
217    }
218
219    #[test]
220    fn test_from_str_rejects_wrong_byte_length() {
221        // Valid hex, but 15 and 17 bytes rather than the required 16.
222        assert!(KeyId::from_str("000102030405060708090a0b0c0d0e").is_err());
223        assert!(KeyId::from_str("000102030405060708090a0b0c0d0e0f10").is_err());
224    }
225
226    #[test]
227    fn test_serde_serializes_to_hex_string() {
228        assert_eq!(
229            serde_json::to_string(&test_key_id()).unwrap(),
230            format!("\"{TEST_KEY_ID_HEX}\"")
231        );
232    }
233
234    #[test]
235    fn test_serde_roundtrip() {
236        let key_id = KeyId::make();
237        let json = serde_json::to_string(&key_id).unwrap();
238
239        assert_eq!(serde_json::from_str::<KeyId>(&json).unwrap(), key_id);
240    }
241
242    #[test]
243    fn test_serde_rejects_malformed_hex() {
244        assert!(serde_json::from_str::<KeyId>("\"nothex\"").is_err());
245    }
246}