bitwarden_crypto/keys/
key_id.rs1use 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
19pub(crate) const KEY_ID_SIZE: usize = 16;
21
22#[derive(Clone, PartialEq, Zeroize)]
26pub struct KeyId([u8; KEY_ID_SIZE]);
27
28impl ConstantTimeEq for KeyId {
32 fn ct_eq(&self, other: &Self) -> subtle::Choice {
33 self.0.ct_eq(&other.0)
34 }
35}
36
37impl KeyId {
44 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 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
89impl 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
97impl 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#[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 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}