1use crate::*;
5use md5::{Digest, Md5};
6use rusqlite::OptionalExtension;
7use std::{fs, path::Path};
8use zeroize::{Zeroize, Zeroizing};
9
10pub struct SavedLoginProjection<'a> {
13 pub auto_auth_key: &'a [u8],
14 pub cookie: &'a [u8],
15 pub key_info: &'a [u8],
16 pub auto_login_flag: u32,
19 pub pc_account_name: Option<&'a str>,
20 pub local_device_id64: Option<u64>,
21}
22fn scalar(input: &[u8]) -> Result<u64> {
23 if input.len() > 10 {
24 return Err(ProfileError::InvalidMetadata);
25 }
26 let mut n = 0u64;
27 for (i, b) in input.iter().copied().enumerate() {
28 if i == 9 && b > 1 {
29 return Err(ProfileError::InvalidMetadata);
30 }
31 n |= u64::from(b & 127) << (7 * i);
32 if b & 128 == 0 {
33 return if i + 1 == input.len() {
34 Ok(n)
35 } else {
36 Err(ProfileError::InvalidMetadata)
37 };
38 }
39 }
40 Err(ProfileError::InvalidMetadata)
41}
42fn scalar_bytes(mut n: u64) -> Vec<u8> {
43 let mut out = vec![];
44 while n >= 128 {
45 out.push((n as u8 & 127) | 128);
46 n >>= 7
47 }
48 out.push(n as u8);
49 out
50}
51pub(crate) fn validate_auto_auth_key(input: &[u8]) -> Result<()> {
52 let fields = crate::projection::proto_fields(input)?;
53 if fields.len() != 2 {
54 return Err(ProfileError::InvalidMetadata);
55 }
56 for tag in [1, 2] {
57 let values = fields
58 .iter()
59 .filter(|f| f.0 == tag && f.1 == 2)
60 .collect::<Vec<_>>();
61 if values.len() != 1 {
62 return Err(ProfileError::InvalidMetadata);
63 }
64 let nested = crate::projection::proto_fields(values[0].2)?;
65 if nested.len() != 2 {
66 return Err(ProfileError::InvalidMetadata);
67 }
68 let lens = nested
69 .iter()
70 .filter(|f| f.0 == 1 && f.1 == 0)
71 .collect::<Vec<_>>();
72 let bytes = nested
73 .iter()
74 .filter(|f| f.0 == 2 && f.1 == 2)
75 .collect::<Vec<_>>();
76 if lens.len() != 1
77 || bytes.len() != 1
78 || bytes[0].2.is_empty()
79 || scalar(lens[0].2)? != bytes[0].2.len() as u64
80 {
81 return Err(ProfileError::InvalidMetadata);
82 }
83 }
84 Ok(())
85}
86pub(crate) fn saved_login_values(
87 username: &str,
88 input: &SavedLoginProjection<'_>,
89) -> Result<Vec<(String, Option<Vec<u8>>)>> {
90 validate_auto_auth_key(input.auto_auth_key)?;
91 if input.cookie.len() > 255 || input.key_info.is_empty() || input.key_info.len() > 1024 * 1024 {
92 return Err(ProfileError::InvalidMetadata);
93 }
94 let mut out = vec![];
95 for (name, bytes) in [
96 ("mmkv_key_auto_auth_key", input.auto_auth_key),
97 ("mmkv_key_latest_login_username", username.as_bytes()),
98 ] {
99 out.push((name.into(), Some(mmkv_string_value(bytes)?.to_vec())))
100 }
101 if !input.cookie.is_empty() {
102 out.push((
103 "mmkv_key_server_id".into(),
104 Some(mmkv_string_value(input.cookie)?.to_vec()),
105 ))
106 }
107 out.push((
108 "mmkv_key_auto_login_flag".into(),
109 Some(scalar_bytes(input.auto_login_flag as u64)),
110 ));
111 if let Some(name) = input.pc_account_name {
112 if name.len() > 4096 || name.chars().any(char::is_control) {
113 return Err(ProfileError::InvalidMetadata);
114 }
115 out.push((
116 "mmkv_key_pc_account_name".into(),
117 Some(mmkv_string_value(name.as_bytes())?.to_vec()),
118 ))
119 }
120 if let Some(local) = input.local_device_id64 {
121 if local == 0 || local > i64::MAX as u64 {
122 return Err(ProfileError::InvalidMetadata);
123 }
124 out.push(("key_wechat_local_id_64".into(), Some(scalar_bytes(local))));
125 }
126 Ok(out)
127}
128#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
129#[serde(rename_all = "snake_case")]
130pub enum KeyInfoSource {
131 LatestDatabaseRow,
132 LegacyFallback,
133}
134pub struct ProfileSavedLogin {
137 pub identity: ProfileIdentity,
138 pub auto_auth_key: Zeroizing<Vec<u8>>,
139 pub cookie: Zeroizing<Vec<u8>>,
140 pub key_info: Zeroizing<Vec<u8>>,
141 pub key_info_source: KeyInfoSource,
142 pub auto_login_flag: u32,
143 pub logout_locked: Option<bool>,
144 pub pc_account_name: Option<Zeroizing<Vec<u8>>>,
145}
146pub fn read_profile_saved_login(
150 home: impl AsRef<Path>,
151 format_key: &[u8],
152 stopped_or_consistent_snapshot: bool,
153) -> Result<ProfileSavedLogin> {
154 if !stopped_or_consistent_snapshot {
155 return Err(ProfileError::SnapshotRequired);
156 }
157 let home = fs::canonicalize(home)?;
158 let global = "xwechat_files/all_users/config/global_config";
159 let data = crate::media::read_cache(&home, Path::new(global), 4 * 1024 * 1024 + 4)?;
160 let meta = crate::media::read_cache(&home, Path::new(&format!("{global}.crc")), 4096)?;
161 let identity = read_mmkv_identity(&data, &meta, format_key)?;
162 crate::official_account_directory(&identity.username, identity.uin)?;
163 let decoded = decode_mmkv_records(&data, &meta, format_key)?;
164 let token = decoded
165 .string_value("mmkv_key_auto_auth_key")?
166 .ok_or(ProfileError::InvalidMetadata)?;
167 validate_auto_auth_key(&token)?;
168 let cookie = decoded
169 .string_value("mmkv_key_server_id")?
170 .unwrap_or_else(|| Zeroizing::new(vec![]));
171 if cookie.len() > 255 {
172 return Err(ProfileError::InvalidMetadata);
173 }
174 let flag = u32::try_from(scalar(
175 decoded
176 .raw_value("mmkv_key_auto_login_flag")
177 .ok_or(ProfileError::InvalidMetadata)?,
178 )?)
179 .map_err(|_| ProfileError::InvalidMetadata)?;
180 let locked = decoded
181 .raw_value("mmkv_key_logout_locked_flag")
182 .map(scalar)
183 .transpose()?
184 .map(|n| match n {
185 0 => Ok(false),
186 1 => Ok(true),
187 _ => Err(ProfileError::InvalidMetadata),
188 })
189 .transpose()?;
190 let pc_account_name = decoded.string_value("mmkv_key_pc_account_name")?;
191 let (key_info, key_info_source) = read_key_info(&home, &identity.username)?;
192 Ok(ProfileSavedLogin {
193 identity,
194 auto_auth_key: token,
195 cookie,
196 key_info,
197 key_info_source,
198 auto_login_flag: flag,
199 logout_locked: locked,
200 pc_account_name,
201 })
202}
203fn read_key_info(home: &Path, username: &str) -> Result<(Zeroizing<Vec<u8>>, KeyInfoSource)> {
204 let (relative, legacy) = key_info_relative(username)?;
205 let source = home.join(&relative);
206 if source.exists() {
207 let temp = wechat_platform::private_tempdir("wechat-key-info-")?;
210 for suffix in ["", "-wal", "-shm"] {
211 let path = format!("{relative}{suffix}");
212 match crate::media::read_cache(home, Path::new(&path), 16 * 1024 * 1024) {
213 Ok(bytes) => fs::write(temp.path().join(format!("key_info.db{suffix}")), bytes)?,
214 Err(ProfileError::NotCached) if !suffix.is_empty() => {}
215 Err(e) => return Err(e),
216 }
217 }
218 let c = rusqlite::Connection::open_with_flags(
219 temp.path().join("key_info.db"),
220 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
221 )?;
222 c.execute_batch("PRAGMA trusted_schema=OFF;PRAGMA temp_store=MEMORY;")?;
223 let expected = ["user_name_md5", "key_md5", "key_info_md5", "key_info_data"];
224 let actual = c
225 .prepare("PRAGMA table_info(LoginKeyInfoTable)")?
226 .query_map([], |r| r.get::<_, String>(1))?
227 .collect::<std::result::Result<Vec<_>, _>>()?;
228 if actual.iter().map(String::as_str).collect::<Vec<_>>() != expected {
229 return Err(ProfileError::UnsupportedSchema("login/key_info.db".into()));
230 }
231 let hash = format!("{:x}", Md5::digest(username.as_bytes()));
232 let row=c.query_row("SELECT key_info_md5,key_info_data FROM LoginKeyInfoTable WHERE user_name_md5=?1 ORDER BY rowid DESC LIMIT 1",[hash],|r|Ok((r.get::<_,Option<String>>(0)?,r.get::<_,Option<Vec<u8>>>(1)?))).optional()?;
233 if let Some((Some(hash), Some(data))) = row {
234 if !data.is_empty()
235 && data.len() <= 1024 * 1024
236 && format!("{:x}", Md5::digest(&data)) == hash
237 {
238 return Ok((Zeroizing::new(data), KeyInfoSource::LatestDatabaseRow));
239 }
240 }
241 }
242 let data = crate::media::read_cache(home, Path::new(&legacy), 1024 * 1024)?;
243 if data.is_empty() {
244 return Err(ProfileError::InvalidMetadata);
245 }
246 Ok((Zeroizing::new(data), KeyInfoSource::LegacyFallback))
247}
248
249pub fn read_profile_key_info(
253 home: impl AsRef<Path>,
254 username: &str,
255 stopped_or_consistent_snapshot: bool,
256) -> Result<(Zeroizing<Vec<u8>>, KeyInfoSource)> {
257 if !stopped_or_consistent_snapshot {
258 return Err(ProfileError::SnapshotRequired);
259 }
260 crate::official_account_directory(username, 1)?;
261 read_key_info(&fs::canonicalize(home)?, username)
262}
263
264pub fn stage_saved_login_projection(
268 data: &[u8],
269 metadata: &[u8],
270 format_key: &[u8],
271 username: &str,
272 input: &SavedLoginProjection<'_>,
273) -> Result<StagedMmkv> {
274 let identity = read_mmkv_identity(data, metadata, format_key)?;
275 if identity.username != username {
276 return Err(ProfileError::AccountMismatch);
277 }
278 let mut values = saved_login_values(username, input)?;
279 let staged = stage_mmkv_values(data, metadata, format_key, &values);
280 values.zeroize();
281 staged
282}
283pub fn publish_legacy_key_info(
287 path: impl AsRef<Path>,
288 bytes: &[u8],
289 expected_sha256: Option<&str>,
290 options: &WriteOptions,
291) -> Result<bool> {
292 use sha2::Sha256;
293 use std::io::Write;
294 if !options.exclusive_ownership_confirmed || options.durable_intent_id.is_empty() {
295 return Err(ProfileError::SnapshotRequired);
296 }
297 if bytes.is_empty()
298 || bytes.len() > 1024 * 1024
299 || expected_sha256.is_some_and(|s| !crate::media::digest_hex(s, 64))
300 {
301 return Err(ProfileError::InvalidMetadata);
302 }
303 let path = path.as_ref();
304 if path.file_name().is_none_or(|n| n != "key_info.dat") {
305 return Err(ProfileError::InvalidQuery);
306 }
307 let parent = path.parent().ok_or(ProfileError::InvalidQuery)?;
308 let canonical = fs::canonicalize(parent)?;
309 if canonical != parent {
310 return Err(ProfileError::UnsupportedFile);
311 }
312 let old = match crate::media::read_cache(parent, Path::new("key_info.dat"), 1024 * 1024) {
313 Ok(old) => Some(Zeroizing::new(old)),
314 Err(ProfileError::NotCached) => None,
315 Err(e) => return Err(e),
316 };
317 if old.as_deref().is_some_and(|old| old.as_slice() == bytes) {
318 return Ok(false);
319 }
320 let matches = match (&old, expected_sha256) {
321 (None, None) => true,
322 (Some(old), Some(expected)) => {
323 hex::encode(Sha256::digest(old.as_slice())) == expected.to_ascii_lowercase()
324 }
325 _ => false,
326 };
327 if !matches {
328 return Err(ProfileError::SourceChanged);
329 }
330 let mut staged = wechat_platform::private_tempfile_in(parent, ".wx-login-")?;
331 staged.write_all(bytes)?;
332 staged.as_file().sync_all()?;
333 if old.is_none() {
334 staged
335 .persist_noclobber(path)
336 .map_err(|e| ProfileError::Io(e.error))?;
337 } else {
338 staged
339 .persist(path)
340 .map_err(|e| ProfileError::Io(e.error))?;
341 }
342 wechat_platform::sync_dir(parent)?;
343 Ok(true)
344}
345
346#[derive(Clone, serde::Serialize, serde::Deserialize)]
349#[serde(rename_all = "camelCase", deny_unknown_fields)]
350pub struct SavedKeyInfoPlan {
351 pub username_sha256: String,
352 pub database_present: bool,
353 pub expected_selection_sha256: Option<String>,
354 pub expected_legacy_sha256: Option<String>,
355}
356#[derive(Clone, Copy, serde::Serialize)]
357#[serde(rename_all = "camelCase")]
358pub struct SavedKeyInfoPublication {
359 pub database_changed: bool,
360 pub legacy_changed: bool,
361}
362fn key_info_relative(username: &str) -> Result<(String, String)> {
363 crate::official_account_directory(username, 1)?;
365 if !crate::media::valid_component(username) {
366 return Err(ProfileError::InvalidMetadata);
367 }
368 Ok((
369 format!("xwechat_files/all_users/login/{username}/key_info.db"),
370 format!(".xwechat/login/{username}/key_info.dat"),
371 ))
372}
373fn login_schema(c: &rusqlite::Connection) -> Result<()> {
374 let columns = c
375 .prepare("PRAGMA table_info(LoginKeyInfoTable)")?
376 .query_map([], |r| {
377 Ok((
378 r.get::<_, String>(1)?,
379 r.get::<_, String>(2)?,
380 r.get::<_, bool>(3)?,
381 r.get::<_, Option<String>>(4)?,
382 r.get::<_, i64>(5)?,
383 ))
384 })?
385 .collect::<std::result::Result<Vec<_>, _>>()?;
386 let expected = [
387 ("user_name_md5", "TEXT"),
388 ("key_md5", "TEXT"),
389 ("key_info_md5", "TEXT"),
390 ("key_info_data", "BLOB"),
391 ]
392 .into_iter()
393 .map(|(n, t)| (n.to_string(), t.to_string(), false, None, 0))
394 .collect::<Vec<_>>();
395 let indices = c
396 .prepare("PRAGMA index_list(LoginKeyInfoTable)")?
397 .query_map([], |r| {
398 Ok((
399 r.get::<_, String>(1)?,
400 r.get::<_, bool>(2)?,
401 r.get::<_, bool>(4)?,
402 ))
403 })?
404 .collect::<std::result::Result<Vec<_>, _>>()?;
405 let triggers: i64 = c.query_row(
406 "SELECT count(*) FROM sqlite_master WHERE type='trigger' AND tbl_name='LoginKeyInfoTable'",
407 [],
408 |r| r.get(0),
409 )?;
410 if columns != expected
411 || indices != vec![("LoginKeyInfoTable_USER_KEYINFO".into(), true, false)]
412 || triggers != 0
413 {
414 return Err(ProfileError::UnsupportedSchema("login/key_info.db".into()));
415 }
416 let indexed = c
417 .prepare("PRAGMA index_info(LoginKeyInfoTable_USER_KEYINFO)")?
418 .query_map([], |r| r.get::<_, String>(2))?
419 .collect::<std::result::Result<Vec<_>, _>>()?;
420 let ordering = c
421 .prepare("PRAGMA index_xinfo(LoginKeyInfoTable_USER_KEYINFO)")?
422 .query_map([], |r| {
423 Ok((
424 r.get::<_, Option<String>>(2)?,
425 r.get::<_, bool>(3)?,
426 r.get::<_, String>(4)?,
427 r.get::<_, bool>(5)?,
428 ))
429 })?
430 .collect::<std::result::Result<Vec<_>, _>>()?;
431 let keys = ordering.into_iter().filter(|row| row.3).collect::<Vec<_>>();
432 let expected_keys = vec![
433 (Some("user_name_md5".into()), false, "BINARY".into(), true),
434 (Some("key_info_md5".into()), false, "BINARY".into(), true),
435 ];
436 if indexed != ["user_name_md5", "key_info_md5"] || keys != expected_keys {
437 return Err(ProfileError::UnsupportedSchema("login/key_info.db".into()));
438 }
439 Ok(())
440}
441type LoginRow = (
442 i64,
443 Option<String>,
444 Option<String>,
445 Option<String>,
446 Option<Vec<u8>>,
447);
448fn selected_login_row(c: &rusqlite::Connection, username: &str) -> Result<Option<LoginRow>> {
449 Ok(c.query_row("SELECT rowid,user_name_md5,key_md5,key_info_md5,key_info_data FROM LoginKeyInfoTable WHERE user_name_md5=?1 ORDER BY rowid DESC LIMIT 1",[format!("{:x}",Md5::digest(username.as_bytes()))],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?,r.get(3)?,r.get(4)?))).optional()?)
450}
451fn login_selection_hash(row: &Option<LoginRow>) -> Result<String> {
452 use sha2::Sha256;
453 let bytes = Zeroizing::new(serde_json::to_vec(row).map_err(|_| ProfileError::InvalidMetadata)?);
454 Ok(hex::encode(Sha256::digest(bytes.as_slice())))
455}
456pub fn plan_saved_key_info(
459 home: impl AsRef<Path>,
460 username: &str,
461 stopped: bool,
462) -> Result<SavedKeyInfoPlan> {
463 use sha2::Sha256;
464 if !stopped {
465 return Err(ProfileError::SnapshotRequired);
466 }
467 let home = fs::canonicalize(home)?;
468 let (relative, legacy) = key_info_relative(username)?;
469 let database = home.join(&relative);
470 let database_present = fs::symlink_metadata(&database).is_ok();
471 let expected_selection_sha256 = if database_present {
472 let temp = wechat_platform::private_tempdir("wechat-key-info-")?;
473 for suffix in ["", "-wal", "-shm"] {
474 match crate::media::read_cache(
475 &home,
476 Path::new(&format!("{relative}{suffix}")),
477 16 * 1024 * 1024,
478 ) {
479 Ok(bytes) => fs::write(temp.path().join(format!("key_info.db{suffix}")), bytes)?,
480 Err(ProfileError::NotCached) if !suffix.is_empty() => {}
481 Err(e) => return Err(e),
482 }
483 }
484 let c = rusqlite::Connection::open_with_flags(
485 temp.path().join("key_info.db"),
486 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
487 )?;
488 c.execute_batch("PRAGMA trusted_schema=OFF;PRAGMA temp_store=MEMORY;")?;
489 login_schema(&c)?;
490 Some(login_selection_hash(&selected_login_row(&c, username)?)?)
491 } else {
492 None
493 };
494 let expected_legacy_sha256 =
495 match crate::media::read_cache(&home, Path::new(&legacy), 1024 * 1024) {
496 Ok(bytes) => Some(hex::encode(Sha256::digest(bytes))),
497 Err(ProfileError::NotCached) => None,
498 Err(e) => return Err(e),
499 };
500 Ok(SavedKeyInfoPlan {
501 username_sha256: hex::encode(Sha256::digest(username.as_bytes())),
502 database_present,
503 expected_selection_sha256,
504 expected_legacy_sha256,
505 })
506}
507fn create_login_parent(home: &Path, relative: &Path) -> Result<()> {
508 let mut path = home.to_path_buf();
509 for part in relative
510 .parent()
511 .ok_or(ProfileError::InvalidQuery)?
512 .components()
513 {
514 path.push(part.as_os_str());
515 match fs::symlink_metadata(&path) {
516 Ok(m) if m.is_dir() && !wechat_platform::is_link(&m) => {}
517 Ok(_) => return Err(ProfileError::UnsupportedFile),
518 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
519 fs::create_dir(&path)?;
520 #[cfg(unix)]
521 {
522 use std::os::unix::fs::PermissionsExt;
523 fs::set_permissions(&path, fs::Permissions::from_mode(0o700))?;
524 }
525 wechat_platform::sync_dir(path.parent().ok_or(ProfileError::InvalidQuery)?)?;
526 }
527 Err(e) => return Err(e.into()),
528 }
529 }
530 Ok(())
531}
532pub fn publish_saved_key_info(
538 home: impl AsRef<Path>,
539 username: &str,
540 plan: &SavedKeyInfoPlan,
541 bytes: &[u8],
542 options: &WriteOptions,
543) -> Result<SavedKeyInfoPublication> {
544 publish_saved_key_info_guarded(home, username, plan, bytes, options, || true)
545}
546pub fn publish_saved_key_info_guarded(
550 home: impl AsRef<Path>,
551 username: &str,
552 plan: &SavedKeyInfoPlan,
553 bytes: &[u8],
554 options: &WriteOptions,
555 mut allowed: impl FnMut() -> bool,
556) -> Result<SavedKeyInfoPublication> {
557 use rusqlite::params;
558 use sha2::Sha256;
559 if !options.exclusive_ownership_confirmed || options.durable_intent_id.is_empty() {
560 return Err(ProfileError::SnapshotRequired);
561 }
562 if bytes.is_empty()
563 || bytes.len() > 1024 * 1024
564 || plan.username_sha256 != hex::encode(Sha256::digest(username.as_bytes()))
565 {
566 return Err(ProfileError::InvalidMetadata);
567 }
568 let home = fs::canonicalize(home)?;
569 let (relative, legacy) = key_info_relative(username)?;
570 let path = home.join(&relative);
571 let present = fs::symlink_metadata(&path).is_ok();
572 if present != plan.database_present {
573 return Err(ProfileError::SourceChanged);
574 }
575 match crate::media::read_cache(&home, Path::new(&legacy), 1024 * 1024) {
578 Ok(old) if old == bytes => {}
579 Ok(old)
580 if plan.expected_legacy_sha256.as_deref()
581 == Some(hex::encode(Sha256::digest(&old)).as_str()) => {}
582 Err(ProfileError::NotCached) if plan.expected_legacy_sha256.is_none() => {}
583 Ok(_) | Err(ProfileError::NotCached) => return Err(ProfileError::SourceChanged),
584 Err(e) => return Err(e),
585 }
586 let mut database_changed = false;
587 if present {
588 if fs::canonicalize(&path)? != path || !fs::symlink_metadata(&path)?.is_file() {
589 return Err(ProfileError::UnsupportedFile);
590 }
591 let mut c = rusqlite::Connection::open_with_flags(
592 &path,
593 rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
594 )?;
595 c.execute_batch(
596 "PRAGMA trusted_schema=OFF;PRAGMA synchronous=FULL;PRAGMA temp_store=MEMORY;",
597 )?;
598 login_schema(&c)?;
599 let tx = c.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
600 let selected = selected_login_row(&tx, username)?;
601 let digest = format!("{:x}", Md5::digest(bytes));
602 let already = selected.as_ref().is_some_and(|r| {
603 r.3.as_deref() == Some(digest.as_str()) && r.4.as_deref() == Some(bytes)
604 });
605 if !already {
606 if Some(login_selection_hash(&selected)?) != plan.expected_selection_sha256 {
607 return Err(ProfileError::SourceChanged);
608 }
609 let user_hash = format!("{:x}", Md5::digest(username.as_bytes()));
610 let duplicate:Option<(Option<String>,Option<Vec<u8>>)>=tx.query_row("SELECT key_md5,key_info_data FROM LoginKeyInfoTable WHERE user_name_md5=?1 AND key_info_md5=?2",params![user_hash,digest],|r|Ok((r.get(0)?,r.get(1)?))).optional()?;
611 if duplicate.as_ref().is_some_and(|(key, data)| {
612 key.as_deref() != Some("") || data.as_deref() != Some(bytes)
613 }) {
614 return Err(ProfileError::InvalidMetadata);
615 }
616 if !allowed() {
617 return Err(ProfileError::SourceChanged);
618 }
619 tx.execute("INSERT OR REPLACE INTO LoginKeyInfoTable(user_name_md5,key_md5,key_info_md5,key_info_data)VALUES(?1,'',?2,?3)",params![user_hash,digest,bytes])?;
620 database_changed = true;
621 }
622 tx.commit()?;
623 }
624 create_login_parent(&home, Path::new(&legacy))?;
625 if !allowed() {
626 return Err(ProfileError::SourceChanged);
627 }
628 let legacy_changed = publish_legacy_key_info(
629 home.join(legacy),
630 bytes,
631 plan.expected_legacy_sha256.as_deref(),
632 options,
633 )?;
634 Ok(SavedKeyInfoPublication {
635 database_changed,
636 legacy_changed,
637 })
638}
639pub fn saved_login_policy(data: &[u8], metadata: &[u8], format_key: &[u8]) -> Result<u32> {
641 let records = decode_mmkv_records(data, metadata, format_key)?;
642 u32::try_from(scalar(
643 records
644 .raw_value("mmkv_key_auto_login_flag")
645 .ok_or(ProfileError::InvalidMetadata)?,
646 )?)
647 .map_err(|_| ProfileError::InvalidMetadata)
648}
649pub fn stage_accepted_saved_login_projection(
653 data: &[u8],
654 metadata: &[u8],
655 format_key: &[u8],
656 username: &str,
657 input: &SavedLoginProjection<'_>,
658) -> Result<StagedMmkv> {
659 let identity = read_mmkv_identity(data, metadata, format_key)?;
660 if identity.username != username {
661 return Err(ProfileError::AccountMismatch);
662 }
663 let mut values = saved_login_values(username, input)?;
664 values.push(("mmkv_key_logout_locked_flag".into(), Some(vec![0])));
665 if input.cookie.is_empty() {
666 values.push((
667 "mmkv_key_server_id".into(),
668 Some(mmkv_string_value(&[])?.to_vec()),
669 ));
670 }
671 let staged = stage_mmkv_values(data, metadata, format_key, &values);
672 values.zeroize();
673 staged
674}