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