ic_auth_client/storage/sync_storage/
pem.rs1use crate::storage::{
7 DecodeError, KEY_STORAGE_KEY, StorageError, StoredKey, sync_storage::AuthClientStorage,
8};
9use base64::prelude::{BASE64_STANDARD_NO_PAD, Engine as _};
10use pkcs8::{
11 LineEnding, ObjectIdentifier, PrivateKeyInfo, SecretDocument, der::pem::PemLabel,
12 spki::AlgorithmIdentifierRef,
13};
14use serde::{Deserialize, Serialize};
15use std::{
16 fs,
17 io::ErrorKind,
18 path::{Path, PathBuf},
19};
20
21const PEM_STORAGE_PREFIX: &str = "ic-";
22const STORAGE_FILE_EXTENSION: &str = "json";
23const KEY_FILE_EXTENSION: &str = "pem";
24const ED25519_OID: &str = "1.3.101.112";
25
26#[derive(Debug, Clone)]
28pub struct PemStorage {
29 directory: PathBuf,
30}
31
32impl PemStorage {
33 pub fn new(directory: PathBuf) -> Self {
43 Self { directory }
44 }
45
46 pub fn import_private_key_from_pem_file<P: AsRef<Path>>(
48 &mut self,
49 path: P,
50 ) -> Result<(), StorageError> {
51 let raw_key = Self::decode_pem_private_key_from_path(path.as_ref())?;
52 self.write_private_key_pem(&raw_key)?;
53 Ok(())
54 }
55
56 fn ensure_directory(&self) -> Result<(), StorageError> {
57 if self.directory.as_os_str().is_empty() {
58 return Ok(()); }
60 fs::create_dir_all(&self.directory)?;
61 Ok(())
62 }
63
64 fn file_path(&self, key: &str) -> PathBuf {
65 let sanitized_key = sanitize_key(key);
66 self.directory.join(format!(
67 "{PEM_STORAGE_PREFIX}{sanitized_key}.{STORAGE_FILE_EXTENSION}"
68 ))
69 }
70
71 fn key_file_path(&self) -> PathBuf {
72 self.directory.join(format!(
73 "{PEM_STORAGE_PREFIX}{KEY_STORAGE_KEY}.{KEY_FILE_EXTENSION}"
74 ))
75 }
76
77 fn read_private_key_pem(&self) -> Result<Option<[u8; 32]>, StorageError> {
78 let path = self.key_file_path();
79 match fs::read_to_string(&path) {
80 Ok(contents) => Self::decode_pem_private_key(&contents).map(Some),
81 Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
82 Err(e) => Err(StorageError::from(e)),
83 }
84 }
85
86 fn write_private_key_pem(&self, key: &[u8; 32]) -> Result<(), StorageError> {
87 self.ensure_directory()?;
88 let algorithm = AlgorithmIdentifierRef {
89 oid: ObjectIdentifier::new_unwrap(ED25519_OID),
90 parameters: None,
91 };
92 let info = PrivateKeyInfo::new(algorithm, key);
93 let document =
94 SecretDocument::encode_msg(&info).map_err(|e| StorageError::File(e.to_string()))?;
95 let pem = document
96 .to_pem(PrivateKeyInfo::PEM_LABEL, LineEnding::LF)
97 .map_err(|e| StorageError::File(e.to_string()))?;
98 fs::write(self.key_file_path(), pem.as_bytes())?;
99 Ok(())
100 }
101
102 fn decode_pem_private_key(contents: &str) -> Result<[u8; 32], StorageError> {
103 let (_, document) =
104 SecretDocument::from_pem(contents).map_err(|e| StorageError::File(e.to_string()))?;
105 let info: PrivateKeyInfo<'_> = document
106 .decode_msg()
107 .map_err(|e| StorageError::File(e.to_string()))?;
108 if info.algorithm.oid != ObjectIdentifier::new_unwrap(ED25519_OID) {
109 return Err(StorageError::Decode(DecodeError::Ed25519(
110 "Unsupported key algorithm".to_string(),
111 )));
112 }
113 let bytes: [u8; 32] = info
114 .private_key
115 .try_into()
116 .map_err(|_| StorageError::Decode(DecodeError::Ed25519("Invalid key length".into())))?;
117 Ok(bytes)
118 }
119
120 fn decode_pem_private_key_from_path(path: &Path) -> Result<[u8; 32], StorageError> {
121 let data = fs::read_to_string(path)?;
122 Self::decode_pem_private_key(&data)
123 }
124
125 fn read_json_value(&self, key: &str) -> Result<Option<StoredKey>, StorageError> {
126 let path = self.file_path(key);
127 match fs::read_to_string(&path) {
128 Ok(contents) => {
129 let value: PemStoredValue = serde_json::from_str(&contents)
130 .map_err(|e| StorageError::File(e.to_string()))?;
131 Ok(Some(StoredKey::try_from(value)?))
132 }
133 Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
134 Err(e) => Err(StorageError::from(e)),
135 }
136 }
137
138 fn write_json_value(&mut self, key: &str, value: StoredKey) -> Result<(), StorageError> {
139 self.ensure_directory()?;
140 let path = self.file_path(key);
141 let serialized = serde_json::to_string(&PemStoredValue::from(&value))
142 .map_err(|e| StorageError::File(e.to_string()))?;
143 fs::write(path, serialized)?;
144 Ok(())
145 }
146
147 fn remove_json_file(&self, key: &str) -> Result<(), StorageError> {
148 let path = self.file_path(key);
149 match fs::remove_file(&path) {
150 Ok(_) => Ok(()),
151 Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
152 Err(e) => Err(StorageError::from(e)),
153 }
154 }
155
156 fn stored_key_to_raw(value: StoredKey) -> Result<[u8; 32], StorageError> {
157 match value {
158 StoredKey::Raw(bytes) => Ok(bytes),
159 StoredKey::String(string) => {
160 let stored = StoredKey::String(string);
161 stored.decode().map_err(StorageError::from)
162 }
163 }
164 }
165}
166
167fn sanitize_key(key: &str) -> String {
168 key.chars()
169 .map(|c| {
170 if matches!(c, '/' | '\\' | ':' | '*') {
171 '_'
172 } else {
173 c
174 }
175 })
176 .collect()
177}
178
179#[derive(Debug, Serialize, Deserialize)]
180#[serde(tag = "type", content = "value")]
181enum PemStoredValue {
182 Raw(String),
183 String(String),
184}
185
186impl From<&StoredKey> for PemStoredValue {
187 fn from(value: &StoredKey) -> Self {
188 match value {
189 StoredKey::Raw(bytes) => {
190 PemStoredValue::Raw(BASE64_STANDARD_NO_PAD.encode(bytes.as_slice()))
191 }
192 StoredKey::String(string) => PemStoredValue::String(string.clone()),
193 }
194 }
195}
196
197impl TryFrom<PemStoredValue> for StoredKey {
198 type Error = DecodeError;
199
200 fn try_from(value: PemStoredValue) -> Result<Self, Self::Error> {
201 match value {
202 PemStoredValue::Raw(data) => {
203 let decoded = BASE64_STANDARD_NO_PAD
204 .decode(data)
205 .map_err(DecodeError::Base64)?;
206 StoredKey::try_from(decoded)
207 }
208 PemStoredValue::String(string) => Ok(StoredKey::String(string)),
209 }
210 }
211}
212
213impl AuthClientStorage for PemStorage {
214 fn get(&mut self, key: &str) -> Result<Option<StoredKey>, StorageError> {
215 if key == KEY_STORAGE_KEY {
216 if let Some(raw_key) = self.read_private_key_pem()? {
217 return Ok(Some(StoredKey::Raw(raw_key)));
218 }
219 if let Some(legacy) = self.read_json_value(key)? {
220 let raw = legacy.decode().map_err(StorageError::from)?;
221 self.write_private_key_pem(&raw)?;
222 let _ = self.remove_json_file(key);
223 return Ok(Some(StoredKey::Raw(raw)));
224 }
225 return Ok(None);
226 }
227 self.read_json_value(key)
228 }
229
230 fn set(&mut self, key: &str, value: StoredKey) -> Result<(), StorageError> {
231 if key == KEY_STORAGE_KEY {
232 let raw = Self::stored_key_to_raw(value)?;
233 self.write_private_key_pem(&raw)?;
234 let _ = self.remove_json_file(key);
235 return Ok(());
236 }
237 self.write_json_value(key, value)
238 }
239
240 fn remove(&mut self, key: &str) -> Result<(), StorageError> {
241 if key == KEY_STORAGE_KEY {
242 let path = self.key_file_path();
243 match fs::remove_file(&path) {
244 Ok(_) => (),
245 Err(e) if e.kind() == ErrorKind::NotFound => (),
246 Err(e) => return Err(StorageError::from(e)),
247 }
248 let _ = self.remove_json_file(key);
249 return Ok(());
250 }
251 self.remove_json_file(key)
252 }
253}
254
255impl From<PemStorage> for Box<dyn AuthClientStorage> {
256 fn from(storage: PemStorage) -> Self {
257 Box::new(storage)
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use std::time::{SystemTime, UNIX_EPOCH};
265
266 fn temp_directory() -> PathBuf {
267 let mut path = std::env::temp_dir();
268 let unique = SystemTime::now()
269 .duration_since(UNIX_EPOCH)
270 .unwrap()
271 .as_nanos();
272 path.push(format!("ic-auth-client-test-{unique}"));
273 path
274 }
275
276 #[test]
277 fn pem_storage_persists_raw_keys() {
278 let dir = temp_directory();
279 let mut storage = PemStorage::new(dir.clone());
280 let key = [42u8; 32];
281 storage
282 .set("identity", StoredKey::from(key))
283 .expect("store key");
284 let retrieved = storage.get("identity").expect("read key").unwrap();
285 assert_eq!(retrieved.decode().unwrap(), key);
286 let _ = fs::remove_dir_all(dir);
287 }
288
289 #[test]
290 fn pem_storage_persists_strings() {
291 let dir = temp_directory();
292 let mut storage = PemStorage::new(dir.clone());
293 storage
294 .set("delegation", StoredKey::String("value".into()))
295 .expect("store value");
296 let retrieved = storage.get("delegation").expect("read value").unwrap();
297 assert_eq!(retrieved.encode(), "value");
298 storage.remove("delegation").expect("remove");
299 let after_remove = storage.get("delegation").expect("read missing");
300 assert!(after_remove.is_none());
301 let _ = fs::remove_dir_all(dir);
302 }
303
304 #[test]
305 fn pem_storage_persists_identity_as_pem() {
306 let dir = temp_directory();
307 let mut storage = PemStorage::new(dir.clone());
308 let key = [7u8; 32];
309 storage
310 .set(KEY_STORAGE_KEY, StoredKey::from(key))
311 .expect("store key");
312 let retrieved = storage
313 .get(KEY_STORAGE_KEY)
314 .expect("read key")
315 .expect("missing key");
316 assert_eq!(retrieved.decode().unwrap(), key);
317 let pem_key = storage
318 .read_private_key_pem()
319 .expect("read pem")
320 .expect("missing pem");
321 assert_eq!(pem_key, key);
322 let _ = fs::remove_dir_all(dir);
323 }
324
325 #[test]
326 fn pem_storage_migrates_legacy_identity_json() {
327 let dir = temp_directory();
328 let mut storage = PemStorage::new(dir.clone());
329 let key = [9u8; 32];
330 storage
331 .write_json_value(KEY_STORAGE_KEY, StoredKey::from(key))
332 .expect("write legacy json");
333 let legacy_path = storage.file_path(KEY_STORAGE_KEY);
334 assert!(legacy_path.exists());
335
336 let retrieved = storage
337 .get(KEY_STORAGE_KEY)
338 .expect("read key")
339 .expect("missing key");
340 assert_eq!(retrieved.decode().unwrap(), key);
341 assert!(storage.key_file_path().exists());
342 assert!(!legacy_path.exists());
343 let _ = fs::remove_dir_all(dir);
344 }
345}