1use crate::{
2 keys::{derive_key, verify_first_page, SecretBytes},
3 DatabaseInventory, DatabaseKey, DatabaseState, KeyFile, ProfileError, Result,
4};
5use rusqlite::{Connection, OpenFlags};
6use sha2::{Digest, Sha256};
7use std::{
8 collections::BTreeMap,
9 fs,
10 io::{Read, Write},
11 path::{Path, PathBuf},
12 sync::{Arc, Mutex, MutexGuard, TryLockError},
13 time::Duration,
14};
15use zeroize::Zeroizing;
16
17#[derive(Clone, Debug)]
18pub struct ProfileLocation {
19 pub(crate) root: PathBuf,
20 pub(crate) storage: PathBuf,
21 pub(crate) account: String,
22}
23impl ProfileLocation {
24 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
27 let p = fs::canonicalize(path).map_err(|_| ProfileError::InvalidProfile)?;
28 let root = if p.file_name().is_some_and(|n| n == "db_storage") {
29 p.parent()
30 .ok_or(ProfileError::InvalidProfile)?
31 .to_path_buf()
32 } else {
33 p
34 };
35 let storage = root.join("db_storage");
36 if !root.is_dir()
37 || !storage.is_dir()
38 || fs::symlink_metadata(&storage)?.file_type().is_symlink()
39 {
40 return Err(ProfileError::InvalidProfile);
41 }
42 let account = root
43 .file_name()
44 .and_then(|s| s.to_str())
45 .ok_or(ProfileError::InvalidProfile)?
46 .to_string();
47 Ok(Self {
48 root,
49 storage,
50 account,
51 })
52 }
53 pub fn root(&self) -> &Path {
54 &self.root
55 }
56 pub fn account_directory(&self) -> &str {
57 &self.account
58 }
59}
60
61#[derive(Clone, Debug)]
62pub struct ReadOptions {
63 pub stopped_or_consistent_snapshot: bool,
66 pub allow_plaintext: bool,
68 pub max_database_bytes: u64,
69 pub max_total_bytes: u64,
70 pub max_databases: usize,
71 pub max_blob_bytes: usize,
72 pub query_timeout: Duration,
73}
74impl Default for ReadOptions {
75 fn default() -> Self {
76 Self {
77 stopped_or_consistent_snapshot: false,
78 allow_plaintext: false,
79 max_database_bytes: 2 * 1024 * 1024 * 1024,
80 max_total_bytes: 8 * 1024 * 1024 * 1024,
81 max_databases: 4096,
82 max_blob_bytes: 16 * 1024 * 1024,
83 query_timeout: Duration::from_secs(10),
84 }
85 }
86}
87
88pub(crate) struct Snapshot {
89 pub location: ProfileLocation,
90 pub keys: KeyFile,
91 pub options: ReadOptions,
92 pub generation: String,
93 pub inventory: Vec<DatabaseInventory>,
94 pub paths: BTreeMap<String, PathBuf>,
95 pub(crate) _temp: Option<wechat_platform::PrivateTempDir>,
96 pub(crate) owned: BTreeMap<String, Arc<Mutex<Connection>>>,
97 pub(crate) pinned: Option<PinnedTransactions>,
98}
99
100pub(crate) struct PinnedTransactions {
101 pub(crate) connections: BTreeMap<String, Vec<Arc<Mutex<Connection>>>>,
102}
103impl Drop for PinnedTransactions {
104 fn drop(&mut self) {
105 for connections in self.connections.values() {
106 for connection in connections {
107 if let Ok(connection) = connection.lock() {
108 connection.progress_handler(0, None::<fn() -> bool>);
109 let _ = connection.execute_batch("ROLLBACK");
110 }
111 }
112 }
113 }
114}
115
116pub(crate) enum ReadConnection<'a> {
117 Copied(Connection),
118 Owned(MutexGuard<'a, Connection>),
119 Pinned(MutexGuard<'a, Connection>),
120}
121impl std::ops::Deref for ReadConnection<'_> {
122 type Target = Connection;
123 fn deref(&self) -> &Connection {
124 match self {
125 Self::Copied(connection) => connection,
126 Self::Owned(connection) | Self::Pinned(connection) => connection,
127 }
128 }
129}
130impl Drop for ReadConnection<'_> {
131 fn drop(&mut self) {
132 if let Self::Owned(connection) = self {
133 connection.progress_handler(0, None::<fn() -> bool>);
134 let _ = connection.execute_batch("ROLLBACK");
135 }
136 }
137}
138
139fn relative(root: &Path, p: &Path) -> Result<String> {
140 p.strip_prefix(root)
141 .ok()
142 .and_then(|v| v.to_str())
143 .map(|v| v.replace('\\', "/"))
144 .ok_or(ProfileError::UnsupportedFile)
145}
146pub(crate) fn walk(root: &Path, out: &mut Vec<PathBuf>, depth: usize, max: usize) -> Result<()> {
147 if depth > 16 {
148 return Err(ProfileError::ResourceLimit);
149 }
150 let mut entries = fs::read_dir(root)?.collect::<std::result::Result<Vec<_>, _>>()?;
151 entries.sort_by_key(|e| e.file_name());
152 for e in entries {
153 let m = fs::symlink_metadata(e.path())?;
154 if wechat_platform::is_link(&m) || (!m.is_file() && !m.is_dir()) {
155 return Err(ProfileError::UnsupportedFile);
156 }
157 if m.is_dir() {
158 walk(&e.path(), out, depth + 1, max)?;
159 } else if e.path().extension().is_some_and(|s| s == "db") {
160 out.push(e.path());
161 if out.len() > max {
162 return Err(ProfileError::ResourceLimit);
163 }
164 }
165 }
166 Ok(())
167}
168fn hash_copy(
169 source: &Path,
170 destination: Option<&Path>,
171 remaining: &mut u64,
172 max: u64,
173) -> Result<(u64, String)> {
174 let m = fs::symlink_metadata(source)?;
175 if !m.is_file() || wechat_platform::is_link(&m) {
176 return Err(ProfileError::UnsupportedFile);
177 }
178 if m.len() > max || m.len() > *remaining {
179 return Err(ProfileError::ResourceLimit);
180 }
181 let mut options = wechat_platform::OpenOptions::new();
182 options.read(true);
183 #[cfg(unix)]
184 {
185 options.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
186 }
187 let mut from = options.open(source)?;
188 if !from.metadata()?.is_file() {
189 return Err(ProfileError::UnsupportedFile);
190 }
191 let mut to = if let Some(destination) = destination {
192 let mut options = wechat_platform::OpenOptions::new();
193 options.write(true).create_new(true);
194 #[cfg(unix)]
195 {
196 options
197 .mode(0o600)
198 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
199 }
200 Some(options.open(destination)?)
201 } else {
202 None
203 };
204 let mut h = Sha256::new();
205 let mut bytes = 0u64;
206 let mut buf = [0u8; 65536];
207 loop {
208 let n = from.read(&mut buf)?;
209 if n == 0 {
210 break;
211 }
212 bytes += n as u64;
213 if bytes > max || bytes > *remaining {
214 return Err(ProfileError::ResourceLimit);
215 }
216 h.update(&buf[..n]);
217 if let Some(f) = &mut to {
218 f.write_all(&buf[..n])?;
219 }
220 }
221 *remaining -= bytes;
222 Ok((bytes, hex::encode(h.finalize())))
223}
224fn candidates(storage: &Path, options: &ReadOptions) -> Result<Vec<PathBuf>> {
225 let mut dbs = Vec::new();
226 walk(storage, &mut dbs, 0, options.max_databases)?;
227 if dbs.is_empty() {
228 return Err(ProfileError::InvalidProfile);
229 }
230 let mut files = Vec::new();
231 for p in dbs {
232 files.push(p.clone());
233 for suffix in ["-wal", "-shm"] {
234 let x = PathBuf::from(format!("{}{suffix}", p.display()));
235 if x.try_exists()? {
236 files.push(x);
237 }
238 }
239 }
240 Ok(files)
241}
242
243impl Snapshot {
244 pub fn create(location: ProfileLocation, keys: KeyFile, options: ReadOptions) -> Result<Self> {
245 if !options.stopped_or_consistent_snapshot {
246 return Err(ProfileError::SnapshotRequired);
247 }
248 if keys.account_binding != location.account {
249 return Err(ProfileError::AccountMismatch);
250 }
251 if options.max_blob_bytes == 0
252 || options.max_blob_bytes > 64 * 1024 * 1024
253 || options.max_databases == 0
254 || options.max_databases > 4096
255 || options.query_timeout.is_zero()
256 {
257 return Err(ProfileError::InvalidQuery);
258 }
259 let temp = wechat_platform::private_tempdir("agent-wechat-read-")?;
260 let temp_root = fs::canonicalize(temp.path())?;
263 #[cfg(unix)]
264 {
265 use std::os::unix::fs::PermissionsExt;
266 fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o700))?;
267 }
268 let mut manifest = BTreeMap::new();
269 let mut total = options.max_total_bytes;
270 let files = candidates(&location.storage, &options)?;
271 for source in &files {
272 let rel = relative(&location.storage, source)?;
273 let dest = temp_root.join(&rel);
274 fs::create_dir_all(dest.parent().ok_or(ProfileError::UnsupportedFile)?)?;
275 let (bytes, hash) =
276 hash_copy(source, Some(&dest), &mut total, options.max_database_bytes)?;
277 manifest.insert(rel, (bytes, hash));
278 }
279 if files != candidates(&location.storage, &options)? {
282 return Err(ProfileError::SourceChanged);
283 }
284 let mut total = options.max_total_bytes;
285 for source in &files {
286 let rel = relative(&location.storage, source)?;
287 if manifest.get(&rel)
288 != Some(&hash_copy(
289 source,
290 None,
291 &mut total,
292 options.max_database_bytes,
293 )?)
294 {
295 return Err(ProfileError::SourceChanged);
296 }
297 }
298 let mut h = Sha256::new();
299 h.update(location.root.as_os_str().as_encoded_bytes());
300 for (rel, (bytes, digest)) in &manifest {
301 h.update(rel.as_bytes());
302 h.update(bytes.to_le_bytes());
303 h.update(digest.as_bytes());
304 }
305 let generation = hex::encode(h.finalize());
306 let mut inventory = Vec::new();
307 let mut paths = BTreeMap::new();
308 for (rel, (bytes, _)) in &manifest {
309 if !rel.ends_with(".db") {
310 continue;
311 }
312 let p = temp_root.join(rel);
313 let mut prefix = [0u8; 16];
314 let mut f = fs::File::open(&p)?;
315 let n = f.read(&mut prefix)?;
316 let plain = n == 16 && prefix == *b"SQLite format 3\0";
317 let mut dbh = Sha256::new();
318 dbh.update(manifest.get(rel).unwrap().1.as_bytes());
319 if let Some((_, digest)) = manifest.get(&format!("{rel}-wal")) {
320 dbh.update(digest.as_bytes());
321 }
322 inventory.push(DatabaseInventory {
323 relative_path: rel.clone(),
324 bytes: *bytes,
325 plaintext: plain,
326 has_wal: manifest.contains_key(&format!("{rel}-wal")),
327 generation: hex::encode(dbh.finalize()),
328 state: DatabaseState::Readable,
329 });
330 paths.insert(rel.clone(), p);
331 }
332 let mut result = Self {
333 location,
334 keys,
335 options,
336 generation,
337 inventory,
338 paths,
339 _temp: Some(temp),
340 owned: BTreeMap::new(),
341 pinned: None,
342 };
343 for i in 0..result.inventory.len() {
344 let rel = result.inventory[i].relative_path.clone();
345 let state = match result.open(&rel) {
346 Ok(_) => DatabaseState::Readable,
347 Err(ProfileError::KeyUnavailable(_)) => DatabaseState::KeyUnavailable,
348 Err(ProfileError::KeyMismatch(_)) => DatabaseState::KeyMismatch,
349 Err(e) => return Err(e),
350 };
351 result.inventory[i].state = state;
352 }
353 Ok(result)
354 }
355 pub fn open(&self, rel: &str) -> Result<ReadConnection<'_>> {
356 if let Some(pinned) = &self.pinned {
357 let connections = pinned
358 .connections
359 .get(rel)
360 .ok_or(ProfileError::InvalidQuery)?;
361 for connection in connections {
362 match connection.try_lock() {
363 Ok(connection) => return Ok(ReadConnection::Pinned(connection)),
364 Err(TryLockError::WouldBlock) => continue,
365 Err(TryLockError::Poisoned(_)) => return Err(ProfileError::Database),
366 }
367 }
368 return Err(ProfileError::ResourceLimit);
370 }
371 if let Some(connection) = self.owned.get(rel) {
372 let connection = match connection.try_lock() {
373 Ok(connection) => connection,
374 Err(TryLockError::WouldBlock) => {
375 return Ok(ReadConnection::Copied(self.open_connection(rel, true)?));
378 }
379 Err(TryLockError::Poisoned(_)) => return Err(ProfileError::Database),
380 };
381 let deadline = std::time::Instant::now() + self.options.query_timeout;
382 connection.progress_handler(1000, Some(move || std::time::Instant::now() > deadline));
383 connection.execute_batch("BEGIN DEFERRED")?;
384 return Ok(ReadConnection::Owned(connection));
385 }
386 Ok(ReadConnection::Copied(
387 self.open_connection(rel, self._temp.is_none())?,
388 ))
389 }
390 pub(crate) fn open_connection(&self, rel: &str, read_only: bool) -> Result<Connection> {
391 let path = self.paths.get(rel).ok_or(ProfileError::InvalidQuery)?;
392 let entry = self
393 .inventory
394 .iter()
395 .find(|d| d.relative_path == rel)
396 .ok_or(ProfileError::InvalidQuery)?;
397 let c = Connection::open_with_flags(
398 path,
399 (if read_only {
400 OpenFlags::SQLITE_OPEN_READ_ONLY
401 } else {
402 OpenFlags::SQLITE_OPEN_READ_WRITE
403 }) | OpenFlags::SQLITE_OPEN_NO_MUTEX
404 | OpenFlags::SQLITE_OPEN_NOFOLLOW,
405 )?;
406 c.busy_timeout(Duration::from_millis(1000))?;
407 if entry.plaintext {
408 if !self.options.allow_plaintext {
409 return Err(ProfileError::KeyUnavailable(rel.into()));
410 }
411 } else {
412 let k = self
413 .keys
414 .databases
415 .get(rel)
416 .ok_or_else(|| ProfileError::KeyUnavailable(rel.into()))?;
417 let mut page = [0u8; 4096];
418 fs::File::open(path)?
419 .read_exact(&mut page)
420 .map_err(|_| ProfileError::KeyMismatch(rel.into()))?;
421 if !verify_first_page(&page, k.raw_page_key.expose(), &k.salt) {
422 return Err(ProfileError::KeyMismatch(rel.into()));
423 }
424 configure_cipher(&c, &k.raw_page_key, rel)?;
425 }
426 c.query_row("SELECT count(*) FROM sqlite_master", [], |r| {
427 r.get::<_, i64>(0)
428 })
429 .map_err(|_| ProfileError::DatabaseOpen(rel.into()))?;
430 c.execute_batch(
431 "PRAGMA query_only=ON; PRAGMA temp_store=MEMORY; PRAGMA trusted_schema=OFF;",
432 )?;
433 c.set_limit(
434 rusqlite::limits::Limit::SQLITE_LIMIT_LENGTH,
435 self.options.max_blob_bytes as i32,
436 );
437 c.set_limit(
438 rusqlite::limits::Limit::SQLITE_LIMIT_SQL_LENGTH,
439 1024 * 1024,
440 );
441 c.set_limit(rusqlite::limits::Limit::SQLITE_LIMIT_ATTACHED, 0);
442 let deadline = std::time::Instant::now() + self.options.query_timeout;
443 c.progress_handler(1000, Some(move || std::time::Instant::now() > deadline));
444 Ok(c)
445 }
446}
447
448pub(crate) fn configure_cipher(c: &Connection, key: &SecretBytes, rel: &str) -> Result<()> {
449 let available = c
450 .query_row("PRAGMA cipher_version", [], |r| r.get::<_, String>(0))
451 .ok()
452 .is_some_and(|s| !s.is_empty());
453 if !available {
454 return Err(ProfileError::SqlcipherUnavailable);
455 }
456 let sql = Zeroizing::new(format!(
457 "PRAGMA key = \"x'{}'\"; PRAGMA cipher_compatibility=4; PRAGMA cipher_page_size=4096;",
458 hex::encode(key.expose())
459 ));
460 c.execute_batch(&sql)
461 .map_err(|_| ProfileError::DatabaseOpen(rel.into()))
462}
463
464pub fn keys_from_password(
468 location: ProfileLocation,
469 password: &[u8],
470 mut options: ReadOptions,
471) -> Result<KeyFile> {
472 if password.len() != 32 {
473 return Err(ProfileError::InvalidKeysFile);
474 }
475 options.allow_plaintext = false;
476 let snap = Snapshot::create(
478 location.clone(),
479 KeyFile::empty(location.account.clone()),
480 options,
481 )?;
482 let mut keys = KeyFile::empty(location.account.clone());
483 for (rel, path) in &snap.paths {
484 let mut page = [0u8; 4096];
485 fs::File::open(path)?
486 .read_exact(&mut page)
487 .map_err(|_| ProfileError::KeyMismatch(rel.clone()))?;
488 if &page[..16] == b"SQLite format 3\0" {
489 return Err(ProfileError::KeyMismatch(rel.clone()));
490 }
491 let mut salt = [0u8; 16];
492 salt.copy_from_slice(&page[..16]);
493 let key = derive_key(password, &salt);
494 if !verify_first_page(&page, key.expose(), &salt) {
495 return Err(ProfileError::KeyMismatch(rel.clone()));
496 }
497 let c = Connection::open(path).map_err(|_| ProfileError::DatabaseOpen(rel.clone()))?;
498 configure_cipher(&c, &key, rel)?;
499 c.query_row("SELECT count(*) FROM sqlite_master", [], |r| {
500 r.get::<_, i64>(0)
501 })
502 .map_err(|_| ProfileError::DatabaseOpen(rel.clone()))?;
503 keys.databases.insert(
504 rel.clone(),
505 DatabaseKey {
506 relative_path: rel.clone(),
507 salt,
508 raw_page_key: key,
509 },
510 );
511 }
512 Ok(keys)
513}
514
515pub fn keys_from_agent_cache(
520 location: ProfileLocation,
521 agent_database: &Path,
522 agent_token: &[u8],
523 session_id: Option<&str>,
524 options: ReadOptions,
525) -> Result<KeyFile> {
526 if !options.stopped_or_consistent_snapshot {
527 return Err(ProfileError::SnapshotRequired);
528 }
529 if agent_token.is_empty() || agent_token.len() > 16384 {
530 return Err(ProfileError::InvalidKeysFile);
531 }
532 let pass = std::str::from_utf8(agent_token)
533 .map_err(|_| ProfileError::InvalidKeysFile)?
534 .trim();
535 if pass.is_empty() || pass.contains('\0') {
536 return Err(ProfileError::InvalidKeysFile);
537 }
538 let temp = wechat_platform::private_tempdir("agent-wechat-key-export-")?;
539 let copied = temp.path().join("agent.db");
540 let mut hashes = BTreeMap::new();
541 let mut total = options.max_total_bytes;
542 #[cfg(unix)]
543 {
544 use std::os::unix::fs::PermissionsExt;
545 fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o700))?;
546 }
547 for suffix in ["", "-wal", "-shm"] {
548 let source = PathBuf::from(format!("{}{suffix}", agent_database.display()));
549 if source.try_exists()? {
550 let dest = PathBuf::from(format!("{}{suffix}", copied.display()));
551 hashes.insert(
552 source.clone(),
553 hash_copy(&source, Some(&dest), &mut total, options.max_database_bytes)?,
554 );
555 }
556 }
557 if !hashes.contains_key(agent_database) {
558 return Err(ProfileError::KeysMissing);
559 }
560 let mut total = options.max_total_bytes;
561 for (source, hash) in &hashes {
562 if &hash_copy(source, None, &mut total, options.max_database_bytes)? != hash {
563 return Err(ProfileError::SourceChanged);
564 }
565 }
566 for suffix in ["", "-wal", "-shm"] {
567 let source = PathBuf::from(format!("{}{suffix}", agent_database.display()));
568 if source.try_exists()? != hashes.contains_key(&source) {
569 return Err(ProfileError::SourceChanged);
570 }
571 }
572 let c = Connection::open(&copied)?;
573 if c.query_row("PRAGMA cipher_version", [], |r| r.get::<_, String>(0))
574 .ok()
575 .is_none()
576 {
577 return Err(ProfileError::SqlcipherUnavailable);
578 }
579 let sql=Zeroizing::new(format!("PRAGMA key='{}'; PRAGMA query_only=ON; PRAGMA temp_store=MEMORY; PRAGMA trusted_schema=OFF;",pass.replace('\'',"''")));
580 c.execute_batch(&sql)
581 .map_err(|_| ProfileError::InvalidKeysFile)?;
582 c.set_limit(rusqlite::limits::Limit::SQLITE_LIMIT_LENGTH, 1024 * 1024);
583 let deadline = std::time::Instant::now() + options.query_timeout;
584 c.progress_handler(1000, Some(move || std::time::Instant::now() > deadline));
585 let mut s=c.prepare("SELECT DISTINCT session_id FROM wechat_keys WHERE account_dir=?1 ORDER BY session_id LIMIT 3").map_err(|_|ProfileError::InvalidKeysFile)?;
586 let sessions = s
587 .query_map([&location.account], |r| r.get::<_, String>(0))
588 .map_err(|_| ProfileError::InvalidKeysFile)?
589 .collect::<std::result::Result<Vec<_>, _>>()
590 .map_err(|_| ProfileError::InvalidKeysFile)?;
591 let selected = match session_id {
592 Some(id) if sessions.iter().any(|s| s == id) => id,
593 None if sessions.len() == 1 => &sessions[0],
594 _ => return Err(ProfileError::InvalidKeysFile),
595 };
596 let mut s=c.prepare("SELECT db_name,hex_key FROM wechat_keys WHERE account_dir=?1 AND session_id=?2 LIMIT 4097").map_err(|_|ProfileError::InvalidKeysFile)?;
597 let mut records = Zeroizing::new(
598 s.query_map(rusqlite::params![location.account, selected], |r| {
599 Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
600 })
601 .map_err(|_| ProfileError::InvalidKeysFile)?
602 .collect::<std::result::Result<Vec<_>, _>>()
603 .map_err(|_| ProfileError::InvalidKeysFile)?,
604 );
605 if records.len() > 4096 {
606 return Err(ProfileError::ResourceLimit);
607 }
608 let mut keys = KeyFile::empty(location.account.clone());
609 let snap = Snapshot::create(location.clone(), keys.clone(), options.clone())?;
610 for (name, key) in records.iter() {
611 if name.starts_with('_') {
612 continue;
613 }
614 let matching = snap
615 .paths
616 .iter()
617 .filter(|(p, _)| p.rsplit('/').next() == Some(name.as_str()))
618 .collect::<Vec<_>>();
619 if matching.len() != 1 {
620 continue;
621 }
622 let (rel, path) = matching[0];
623 let mut page = [0u8; 4096];
624 fs::File::open(path)?.read_exact(&mut page)?;
625 let salt: [u8; 16] = page[..16]
626 .try_into()
627 .map_err(|_| ProfileError::InvalidKeysFile)?;
628 let key = SecretBytes::new(hex::decode(key).map_err(|_| ProfileError::InvalidKeysFile)?);
629 if key.expose().len() != 32 || !verify_first_page(&page, key.expose(), &salt) {
630 return Err(ProfileError::KeyMismatch(rel.clone()));
631 }
632 let test = Connection::open(path)?;
633 configure_cipher(&test, &key, rel)?;
634 test.query_row("SELECT count(*) FROM sqlite_master", [], |r| {
635 r.get::<_, i64>(0)
636 })
637 .map_err(|_| ProfileError::KeyMismatch(rel.clone()))?;
638 keys.databases.insert(
639 rel.clone(),
640 DatabaseKey {
641 relative_path: rel.clone(),
642 salt,
643 raw_page_key: key,
644 },
645 );
646 }
647 let aes = records
648 .iter()
649 .filter(|(n, _)| n == "_image_aes")
650 .collect::<Vec<_>>();
651 let xor = records
652 .iter()
653 .filter(|(n, _)| n == "_image_xor")
654 .collect::<Vec<_>>();
655 if aes.len() > 1 || xor.len() > 1 {
656 return Err(ProfileError::InvalidKeysFile);
657 }
658 if let (Some(a), Some(x)) = (aes.first(), xor.first()) {
659 let x = u8::from_str_radix(&x.1, 16).map_err(|_| ProfileError::InvalidKeysFile)?;
660 keys.image_dat = Some(crate::ImageDatKey::from_legacy_cache(&a.1, x)?);
661 }
662 for (_, v) in records.iter_mut() {
663 zeroize::Zeroize::zeroize(v);
664 }
665 if keys.databases.is_empty() {
666 return Err(ProfileError::KeyUnavailable(
667 "authorized agent cache".into(),
668 ));
669 }
670 Ok(keys)
671}