Browse Source
Add the remaining utility scripts and deep TSforge store layers: - util/extract_oem — $OEM$ / SetupComplete.cmd generation - util/troubleshoot — SPP/licensing reset actions + HWID partial-key table - Ohook product tables (ohook_data) and planner - libtsforge: constants, product_key packing/base24, and the RSA/AES/HMAC crypto backend (behind the `crypto` feature) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>pull/1516/head
11 changed files with 3071 additions and 21 deletions
@ -0,0 +1,127 @@ |
|||||||
|
//! Verbatim constant blobs ported from LibTSforge `Constants.cs`.
|
||||||
|
//!
|
||||||
|
//! Every byte here is transcribed BYTE-EXACT from the C# reference
|
||||||
|
//! (`TSforge_Activation.cmd`, git `f34d025`). A single wrong byte in a KMS
|
||||||
|
//! response or the HWID block produces a ticket the SPP will reject, so these
|
||||||
|
//! are pinned by length (and treated as opaque data — do not "tidy" them).
|
||||||
|
|
||||||
|
use crate::common::PsVersion; |
||||||
|
|
||||||
|
/// `Constants.UniversalHWIDBlock` — 38-byte hardware-id block.
|
||||||
|
pub const UNIVERSAL_HWID_BLOCK: &[u8] = &[ |
||||||
|
0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||||
|
0x00, 0x00, 0x01, 0x0C, 0x01, 0x00, |
||||||
|
]; |
||||||
|
|
||||||
|
/// `Constants.KMSv4Response` — 158-byte canned KMS v4 activation response.
|
||||||
|
pub const KMS_V4_RESPONSE: &[u8] = &[ |
||||||
|
0x00, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0x30, 0x00, 0x35, 0x00, 0x34, 0x00, 0x32, 0x00, |
||||||
|
0x36, 0x00, 0x2D, 0x00, 0x30, 0x00, 0x30, 0x00, 0x32, 0x00, 0x30, 0x00, 0x36, 0x00, 0x2D, 0x00, |
||||||
|
0x31, 0x00, 0x36, 0x00, 0x31, 0x00, 0x2D, 0x00, 0x36, 0x00, 0x35, 0x00, 0x35, 0x00, 0x35, 0x00, |
||||||
|
0x30, 0x00, 0x36, 0x00, 0x2D, 0x00, 0x30, 0x00, 0x33, 0x00, 0x2D, 0x00, 0x31, 0x00, 0x30, 0x00, |
||||||
|
0x33, 0x00, 0x33, 0x00, 0x2D, 0x00, 0x39, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x2E, 0x00, |
||||||
|
0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x2D, 0x00, 0x30, 0x00, 0x36, 0x00, 0x35, 0x00, |
||||||
|
0x32, 0x00, 0x30, 0x00, 0x31, 0x00, 0x33, 0x00, 0x00, 0x00, 0xDE, 0x19, 0x02, 0xCF, 0x1F, 0x35, |
||||||
|
0x97, 0x4E, 0x8A, 0x8F, 0xB8, 0x07, 0xB1, 0x92, 0xB5, 0xB5, 0x97, 0x42, 0xEC, 0x3A, 0x76, 0x84, |
||||||
|
0xD5, 0x01, 0x32, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x60, 0x27, 0x00, 0x00, 0xC4, 0x1E, |
||||||
|
0xAA, 0x8B, 0xDD, 0x0C, 0xAB, 0x55, 0x6A, 0xCE, 0xAF, 0xAC, 0x7F, 0x5F, 0xBD, 0xE9, |
||||||
|
]; |
||||||
|
|
||||||
|
/// `Constants.KMSv5Response` — 228-byte canned KMS v5 activation response.
|
||||||
|
pub const KMS_V5_RESPONSE: &[u8] = &[ |
||||||
|
0x00, 0x00, 0x05, 0x00, 0xBE, 0x96, 0xF9, 0x04, 0x54, 0x17, 0x3F, 0xAF, 0xE3, 0x08, 0x50, 0xEB, |
||||||
|
0x22, 0xBA, 0x53, 0xBF, 0xF2, 0x6A, 0x7B, 0xC9, 0x05, 0x1D, 0xB5, 0x19, 0xDF, 0x98, 0xE2, 0x71, |
||||||
|
0x4D, 0x00, 0x61, 0xE9, 0x9D, 0x03, 0xFB, 0x31, 0xF9, 0x1F, 0x2E, 0x60, 0x59, 0xC7, 0x73, 0xC8, |
||||||
|
0xE8, 0xB6, 0xE1, 0x2B, 0x39, 0xC6, 0x35, 0x0E, 0x68, 0x7A, 0xAA, 0x4F, 0x28, 0x23, 0x12, 0x18, |
||||||
|
0xE3, 0xAA, 0x84, 0x81, 0x6E, 0x82, 0xF0, 0x3F, 0xD9, 0x69, 0xA9, 0xDF, 0xBA, 0x5F, 0xCA, 0x32, |
||||||
|
0x54, 0xB2, 0x52, 0x3B, 0x3E, 0xD1, 0x5C, 0x65, 0xBC, 0x3E, 0x59, 0x0D, 0x15, 0x9F, 0x37, 0xEC, |
||||||
|
0x30, 0x9C, 0xCC, 0x1B, 0x39, 0x0D, 0x21, 0x32, 0x29, 0xA2, 0xDD, 0xC7, 0xC1, 0x69, 0xF2, 0x72, |
||||||
|
0x3F, 0x00, 0x98, 0x1E, 0xF8, 0x9A, 0x79, 0x44, 0x5D, 0x25, 0x80, 0x7B, 0xF5, 0xE1, 0x7C, 0x68, |
||||||
|
0x25, 0xAA, 0x0D, 0x67, 0x98, 0xE5, 0x59, 0x9B, 0x04, 0xC1, 0x23, 0x33, 0x48, 0xFB, 0x28, 0xD0, |
||||||
|
0x76, 0xDF, 0x01, 0x56, 0xE7, 0xEC, 0xBF, 0x1A, 0xA2, 0x22, 0x28, 0xCA, 0xB1, 0xB4, 0x4C, 0x30, |
||||||
|
0x14, 0x6F, 0xD2, 0x2E, 0x01, 0x2A, 0x04, 0xE3, 0xBD, 0xA7, 0x41, 0x2F, 0xC9, 0xEF, 0x53, 0xC0, |
||||||
|
0x70, 0x48, 0xF1, 0xB2, 0xB6, 0xEA, 0xE7, 0x0F, 0x7A, 0x15, 0xD1, 0xA6, 0xFE, 0x23, 0xC8, 0xF3, |
||||||
|
0xE1, 0x02, 0x9E, 0xA0, 0x4E, 0xBD, 0xF5, 0xEA, 0x53, 0x74, 0x8E, 0x74, 0xA1, 0xA1, 0xBD, 0xBE, |
||||||
|
0x66, 0xC4, 0x73, 0x8F, 0x24, 0xA7, 0x2A, 0x2F, 0xE3, 0xD9, 0xF4, 0x28, 0xD9, 0xF8, 0xA3, 0x93, |
||||||
|
0x03, 0x9E, 0x29, 0xAB, |
||||||
|
]; |
||||||
|
|
||||||
|
/// `Constants.KMSv6Response` — 260-byte canned KMS v6 activation response.
|
||||||
|
pub const KMS_V6_RESPONSE: &[u8] = &[ |
||||||
|
0x00, 0x00, 0x06, 0x00, 0x54, 0xD3, 0x40, 0x08, 0xF3, 0xCD, 0x03, 0xEF, 0xC8, 0x15, 0x87, 0x9E, |
||||||
|
0xCA, 0x2E, 0x85, 0xFB, 0xE6, 0xF6, 0x73, 0x66, 0xFB, 0xDA, 0xBB, 0x7B, 0xB1, 0xBC, 0xD6, 0xF9, |
||||||
|
0x5C, 0x41, 0xA0, 0xFE, 0xE1, 0x74, 0xC4, 0xBB, 0x91, 0xE5, 0xDE, 0x6D, 0x3A, 0x11, 0xD5, 0xFC, |
||||||
|
0x68, 0xC0, 0x7B, 0x82, 0xB2, 0x24, 0xD1, 0x85, 0xBA, 0x45, 0xBF, 0xF1, 0x26, 0xFA, 0xA5, 0xC6, |
||||||
|
0x61, 0x70, 0x69, 0x69, 0x6E, 0x0F, 0x0B, 0x60, 0xB7, 0x3D, 0xE8, 0xF1, 0x47, 0x0B, 0x65, 0xFD, |
||||||
|
0xA7, 0x30, 0x1E, 0xF6, 0xA4, 0xD0, 0x79, 0xC4, 0x58, 0x8D, 0x81, 0xFD, 0xA7, 0xE7, 0x53, 0xF1, |
||||||
|
0x67, 0x78, 0xF0, 0x0F, 0x60, 0x8F, 0xC8, 0x16, 0x35, 0x22, 0x94, 0x48, 0xCB, 0x0F, 0x8E, 0xB2, |
||||||
|
0x1D, 0xF7, 0x3E, 0x28, 0x42, 0x55, 0x6B, 0x07, 0xE3, 0xE8, 0x51, 0xD5, 0xFA, 0x22, 0x0C, 0x86, |
||||||
|
0x65, 0x0D, 0x3F, 0xDD, 0x8D, 0x9B, 0x1B, 0xC9, 0xD3, 0xB8, 0x3A, 0xEC, 0xF1, 0x11, 0x19, 0x25, |
||||||
|
0xF7, 0x84, 0x4A, 0x4C, 0x0A, 0xB5, 0x31, 0x94, 0x37, 0x76, 0xCE, 0xE7, 0xAB, 0xA9, 0x69, 0xDF, |
||||||
|
0xA4, 0xC9, 0x22, 0x6C, 0x23, 0xFF, 0x6B, 0xFC, 0xDA, 0x78, 0xD8, 0xC4, 0x8F, 0x74, 0xBB, 0x26, |
||||||
|
0x05, 0x00, 0x98, 0x9B, 0xE5, 0xE2, 0xAD, 0x0D, 0x57, 0x95, 0x80, 0x66, 0x8E, 0x43, 0x74, 0x87, |
||||||
|
0x93, 0x1F, 0xF4, 0xB2, 0x2C, 0x20, 0x5F, 0xD8, 0x9C, 0x4C, 0x56, 0xB3, 0x57, 0x44, 0x62, 0x68, |
||||||
|
0x8D, 0xAA, 0x40, 0x11, 0x9D, 0x84, 0x62, 0x0E, 0x43, 0x8A, 0x1D, 0xF0, 0x1C, 0x49, 0xD8, 0x56, |
||||||
|
0xEF, 0x4C, 0xD3, 0x64, 0xBA, 0x0D, 0xEF, 0x87, 0xB5, 0x2C, 0x88, 0xF3, 0x18, 0xFF, 0x3A, 0x8C, |
||||||
|
0xF5, 0xA6, 0x78, 0x5C, 0x62, 0xE3, 0x9E, 0x4C, 0xB6, 0x31, 0x2D, 0x06, 0x80, 0x92, 0xBC, 0x2E, |
||||||
|
0x92, 0xA6, 0x56, 0x96, |
||||||
|
]; |
||||||
|
|
||||||
|
/// `Constants.ZeroCID` — a confirmation ID of 48 ASCII `'0'` characters.
|
||||||
|
pub const ZERO_CID: &str = "000000000000000000000000000000000000000000000000"; |
||||||
|
|
||||||
|
/// `Constants.TimerMax` = `TimeSpan.FromMinutes(2147483640).Ticks`.
|
||||||
|
/// A .NET tick is 100 ns, so a minute is `60 * 10_000_000` ticks; the minute
|
||||||
|
/// count is `2^31 - 8`.
|
||||||
|
pub const TIMER_MAX: u64 = 2_147_483_640 * 60 * 10_000_000; |
||||||
|
|
||||||
|
/// `PhysStoreCrypto.EncryptPhysicalStore` envelope-version table. The runtime
|
||||||
|
/// source of truth is [`PsVersion::envelope_version`]; this is the verbatim
|
||||||
|
/// data form, cross-checked against it in tests.
|
||||||
|
pub const ENVELOPE_VERSION_TABLE: &[(PsVersion, u32)] = &[ |
||||||
|
(PsVersion::Vista, 2), |
||||||
|
(PsVersion::Win7, 5), |
||||||
|
(PsVersion::Win8, 1), |
||||||
|
(PsVersion::WinBlue, 2), |
||||||
|
(PsVersion::WinModern, 3), |
||||||
|
]; |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn blob_lengths_are_byte_exact() { |
||||||
|
assert_eq!(UNIVERSAL_HWID_BLOCK.len(), 38); |
||||||
|
assert_eq!(KMS_V4_RESPONSE.len(), 158); |
||||||
|
assert_eq!(KMS_V5_RESPONSE.len(), 228); |
||||||
|
assert_eq!(KMS_V6_RESPONSE.len(), 260); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn kms_responses_carry_their_version_header() { |
||||||
|
// Bytes [0..4] are the little-endian response version (4 / 5 / 6).
|
||||||
|
assert_eq!(&KMS_V4_RESPONSE[..4], &[0x00, 0x00, 0x04, 0x00]); |
||||||
|
assert_eq!(&KMS_V5_RESPONSE[..4], &[0x00, 0x00, 0x05, 0x00]); |
||||||
|
assert_eq!(&KMS_V6_RESPONSE[..4], &[0x00, 0x00, 0x06, 0x00]); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn zero_cid_is_48_zeros() { |
||||||
|
assert_eq!(ZERO_CID.len(), 48); |
||||||
|
assert!(ZERO_CID.bytes().all(|b| b == b'0')); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn timer_max_matches_the_dotnet_expression() { |
||||||
|
assert_eq!(TIMER_MAX, 1_288_490_184_000_000_000); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn envelope_table_agrees_with_common() { |
||||||
|
for &(v, expected) in ENVELOPE_VERSION_TABLE { |
||||||
|
assert_eq!(v.envelope_version(), expected); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,449 @@ |
|||||||
|
//! `RealCrypto`: the RSA/AES/HMAC backend for a *signed* trusted-store ticket.
|
||||||
|
//!
|
||||||
|
//! Faithful port of LibTSforge `CryptoUtils.cs`, `Keys.cs` and
|
||||||
|
//! `PhysStoreCrypto.cs` (git f34d025, embedded in the deleted
|
||||||
|
//! `TSforge_Activation.cmd`). It implements [`TicketCrypto`] plus the
|
||||||
|
//! `data.dat` "UNTRUSTSTORE" envelope encode/decode.
|
||||||
|
//!
|
||||||
|
//! This module is feature-gated (`#[cfg(feature = "crypto")]`) and pulls in
|
||||||
|
//! RustCrypto crates, so — unlike the rest of `libtsforge` — it does **not**
|
||||||
|
//! compile offline. That is expected; the pure store/CRC/hash layers stay
|
||||||
|
//! dependency-free and testable without this feature.
|
||||||
|
//!
|
||||||
|
//! # Byte order
|
||||||
|
//!
|
||||||
|
//! The embedded keys are CryptoAPI `PRIVATEKEYBLOB`s: every integer field is
|
||||||
|
//! **little-endian**. `RSACryptoServiceProvider.ImportCspBlob` reverses those
|
||||||
|
//! fields to the big-endian integers the math needs; [`parse_blob`] reproduces
|
||||||
|
//! that reversal for the RustCrypto `rsa` crate. The signature/ciphertext
|
||||||
|
//! *outputs*, by contrast, need no reversal: .NET's managed `RSACryptoServiceProvider`
|
||||||
|
//! already presents standard big-endian PKCS#1 bytes, exactly what `rsa` emits
|
||||||
|
//! — the C# `PhysStoreCrypto` never calls `.Reverse()` on them.
|
||||||
|
|
||||||
|
use crate::common::{PsVersion, AES_KEY}; |
||||||
|
use crate::crypto::TicketCrypto; |
||||||
|
use std::fmt; |
||||||
|
|
||||||
|
use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; |
||||||
|
use hmac::{Hmac, Mac}; |
||||||
|
use rand::{rngs::OsRng, RngCore}; |
||||||
|
use rsa::{BigUint, Pkcs1v15Encrypt, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey}; |
||||||
|
use sha1::{Digest, Sha1}; |
||||||
|
|
||||||
|
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>; |
||||||
|
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>; |
||||||
|
type HmacSha1 = Hmac<Sha1>; |
||||||
|
|
||||||
|
/// Production SPP trusted-store RSA-1024 private key (`Keys.PRODUCTION`),
|
||||||
|
/// CryptoAPI `PRIVATEKEYBLOB`, transcribed byte-exact.
|
||||||
|
pub static PRODUCTION: [u8; 596] = [ |
||||||
|
0x07, 0x02, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x52, 0x53, 0x41, 0x32, 0x00, 0x04, 0x00, 0x00, |
||||||
|
0x01, 0x00, 0x01, 0x00, 0x29, 0x87, 0xba, 0x3f, 0x52, 0x90, 0x57, 0xd8, 0x12, 0x26, 0x6b, 0x38, |
||||||
|
0xb2, 0x3b, 0xf9, 0x67, 0x08, 0x4f, 0xdd, 0x8b, 0xf5, 0xe3, 0x11, 0xb8, 0x61, 0x3a, 0x33, 0x42, |
||||||
|
0x51, 0x65, 0x05, 0x86, 0x1e, 0x00, 0x41, 0xde, 0xc5, 0xdd, 0x44, 0x60, 0x56, 0x3d, 0x14, 0x39, |
||||||
|
0xb7, 0x43, 0x65, 0xe9, 0xf7, 0x2b, 0xa5, 0xf0, 0xa3, 0x65, 0x68, 0xe9, 0xe4, 0x8b, 0x5c, 0x03, |
||||||
|
0x2d, 0x36, 0xfe, 0x28, 0x4c, 0xd1, 0x3c, 0x3d, 0xc1, 0x90, 0x75, 0xf9, 0x6e, 0x02, 0xe0, 0x58, |
||||||
|
0x97, 0x6a, 0xca, 0x80, 0x02, 0x42, 0x3f, 0x6c, 0x15, 0x85, 0x4d, 0x83, 0x23, 0x6a, 0x95, 0x9e, |
||||||
|
0x38, 0x52, 0x59, 0x38, 0x6a, 0x99, 0xf0, 0xb5, 0xcd, 0x53, 0x7e, 0x08, 0x7c, 0xb5, 0x51, 0xd3, |
||||||
|
0x8f, 0xa3, 0x0d, 0xa0, 0xfa, 0x8d, 0x87, 0x3c, 0xfc, 0x59, 0x21, 0xd8, 0x2e, 0xd9, 0x97, 0x8b, |
||||||
|
0x40, 0x60, 0xb1, 0xd7, 0x2b, 0x0a, 0x6e, 0x60, 0xb5, 0x50, 0xcc, 0x3c, 0xb1, 0x57, 0xe4, 0xb7, |
||||||
|
0xdc, 0x5a, 0x4d, 0xe1, 0x5c, 0xe0, 0x94, 0x4c, 0x5e, 0x28, 0xff, 0xfa, 0x80, 0x6a, 0x13, 0x53, |
||||||
|
0x52, 0xdb, 0xf3, 0x04, 0x92, 0x43, 0x38, 0xb9, 0x1b, 0xd9, 0x85, 0x54, 0x7b, 0x14, 0xc7, 0x89, |
||||||
|
0x16, 0x8a, 0x4b, 0x82, 0xa1, 0x08, 0x02, 0x99, 0x23, 0x48, 0xdd, 0x75, 0x9c, 0xc8, 0xc1, 0xce, |
||||||
|
0xb0, 0xd7, 0x1b, 0xd8, 0xfb, 0x2d, 0xa7, 0x2e, 0x47, 0xa7, 0x18, 0x4b, 0xf6, 0x29, 0x69, 0x44, |
||||||
|
0x30, 0x33, 0xba, 0xa7, 0x1f, 0xce, 0x96, 0x9e, 0x40, 0xe1, 0x43, 0xf0, 0xe0, 0x0d, 0x0a, 0x32, |
||||||
|
0xb4, 0xee, 0xa1, 0xc3, 0x5e, 0x9b, 0xc7, 0x7f, 0xf5, 0x9d, 0xd8, 0xf2, 0x0f, 0xd9, 0x8f, 0xad, |
||||||
|
0x75, 0x0a, 0x00, 0xd5, 0x25, 0x43, 0xf7, 0xae, 0x51, 0x7f, 0xb7, 0xde, 0xb7, 0xad, 0xfb, 0xce, |
||||||
|
0x83, 0xe1, 0x81, 0xff, 0xdd, 0xa2, 0x77, 0xfe, 0xeb, 0x27, 0x1f, 0x10, 0xfa, 0x82, 0x37, 0xf4, |
||||||
|
0x7e, 0xcc, 0xe2, 0xa1, 0x58, 0xc8, 0xaf, 0x1d, 0x1a, 0x81, 0x31, 0x6e, 0xf4, 0x8b, 0x63, 0x34, |
||||||
|
0xf3, 0x05, 0x0f, 0xe1, 0xcc, 0x15, 0xdc, 0xa4, 0x28, 0x7a, 0x9e, 0xeb, 0x62, 0xd8, 0xd8, 0x8c, |
||||||
|
0x85, 0xd7, 0x07, 0x87, 0x90, 0x2f, 0xf7, 0x1c, 0x56, 0x85, 0x2f, 0xef, 0x32, 0x37, 0x07, 0xab, |
||||||
|
0xb0, 0xe6, 0xb5, 0x02, 0x19, 0x35, 0xaf, 0xdb, 0xd4, 0xa2, 0x9c, 0x36, 0x80, 0xc6, 0xdc, 0x82, |
||||||
|
0x08, 0xe0, 0xc0, 0x5f, 0x3c, 0x59, 0xaa, 0x4e, 0x26, 0x03, 0x29, 0xb3, 0x62, 0x58, 0x41, 0x59, |
||||||
|
0x3a, 0x37, 0x43, 0x35, 0xe3, 0x9f, 0x34, 0xe2, 0xa1, 0x04, 0x97, 0x12, 0x9d, 0x8c, 0xad, 0xf7, |
||||||
|
0xfb, 0x8c, 0xa1, 0xa2, 0xe9, 0xe4, 0xef, 0xd9, 0xc5, 0xe5, 0xdf, 0x0e, 0xbf, 0x4a, 0xe0, 0x7a, |
||||||
|
0x1e, 0x10, 0x50, 0x58, 0x63, 0x51, 0xe1, 0xd4, 0xfe, 0x57, 0xb0, 0x9e, 0xd7, 0xda, 0x8c, 0xed, |
||||||
|
0x7d, 0x82, 0xac, 0x2f, 0x25, 0x58, 0x0a, 0x58, 0xe6, 0xa4, 0xf4, 0x57, 0x4b, 0xa4, 0x1b, 0x65, |
||||||
|
0xb9, 0x4a, 0x87, 0x46, 0xeb, 0x8c, 0x0f, 0x9a, 0x48, 0x90, 0xf9, 0x9f, 0x76, 0x69, 0x03, 0x72, |
||||||
|
0x77, 0xec, 0xc1, 0x42, 0x4c, 0x87, 0xdb, 0x0b, 0x3c, 0xd4, 0x74, 0xef, 0xe5, 0x34, 0xe0, 0x32, |
||||||
|
0x45, 0xb0, 0xf8, 0xab, 0xd5, 0x26, 0x21, 0xd7, 0xd2, 0x98, 0x54, 0x8f, 0x64, 0x88, 0x20, 0x2b, |
||||||
|
0x14, 0xe3, 0x82, 0xd5, 0x2a, 0x4b, 0x8f, 0x4e, 0x35, 0x20, 0x82, 0x7e, 0x1b, 0xfe, 0xfa, 0x2c, |
||||||
|
0x79, 0x6c, 0x6e, 0x66, 0x94, 0xbb, 0x0a, 0xeb, 0xba, 0xd9, 0x70, 0x61, 0xe9, 0x47, 0xb5, 0x82, |
||||||
|
0xfc, 0x18, 0x3c, 0x66, 0x3a, 0x09, 0x2e, 0x1f, 0x61, 0x74, 0xca, 0xcb, 0xf6, 0x7a, 0x52, 0x37, |
||||||
|
0x1d, 0xac, 0x8d, 0x63, 0x69, 0x84, 0x8e, 0xc7, 0x70, 0x59, 0xdd, 0x2d, 0x91, 0x1e, 0xf7, 0xb1, |
||||||
|
0x56, 0xed, 0x7a, 0x06, 0x9d, 0x5b, 0x33, 0x15, 0xdd, 0x31, 0xd0, 0xe6, 0x16, 0x07, 0x9b, 0xa5, |
||||||
|
0x94, 0x06, 0x7d, 0xc1, 0xe9, 0xd6, 0xc8, 0xaf, 0xb4, 0x1e, 0x2d, 0x88, 0x06, 0xa7, 0x63, 0xb8, |
||||||
|
0xcf, 0xc8, 0xa2, 0x6e, 0x84, 0xb3, 0x8d, 0xe5, 0x47, 0xe6, 0x13, 0x63, 0x8e, 0xd1, 0x7f, 0xd4, |
||||||
|
0x81, 0x44, 0x38, 0xbf, |
||||||
|
]; |
||||||
|
|
||||||
|
/// Test SPP trusted-store RSA-1024 private key (`Keys.TEST`), CryptoAPI
|
||||||
|
/// `PRIVATEKEYBLOB`, transcribed byte-exact.
|
||||||
|
pub static TEST: [u8; 596] = [ |
||||||
|
0x07, 0x02, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x52, 0x53, 0x41, 0x32, 0x00, 0x04, 0x00, 0x00, |
||||||
|
0x01, 0x00, 0x01, 0x00, 0x0f, 0xbe, 0x77, 0xb8, 0xdd, 0x54, 0x36, 0xdd, 0x67, 0xd4, 0x17, 0x66, |
||||||
|
0xc4, 0x13, 0xd1, 0x3f, 0x1e, 0x16, 0x0c, 0x16, 0x35, 0xab, 0x6d, 0x3d, 0x34, 0x51, 0xed, 0x3f, |
||||||
|
0x57, 0x14, 0xb6, 0xb7, 0x08, 0xe9, 0xd9, 0x7a, 0x80, 0xb3, 0x5f, 0x9b, 0x3a, 0xfd, 0x9e, 0x37, |
||||||
|
0x3a, 0x53, 0x72, 0x67, 0x92, 0x60, 0xc3, 0xef, 0xb5, 0x8e, 0x1e, 0xcf, 0x9d, 0x9c, 0xd3, 0x90, |
||||||
|
0xe5, 0xdd, 0xf4, 0xdb, 0xf3, 0xd6, 0x65, 0xb3, 0xc1, 0xbd, 0x69, 0xe1, 0x76, 0x95, 0xd9, 0x37, |
||||||
|
0xb8, 0x5e, 0xca, 0x3d, 0x98, 0xfc, 0x50, 0x5c, 0x98, 0xae, 0xe3, 0x7c, 0x4c, 0x27, 0xc3, 0xd0, |
||||||
|
0xce, 0x78, 0x06, 0x51, 0x68, 0x23, 0xe6, 0x70, 0xf8, 0x7c, 0xae, 0x36, 0xbe, 0x41, 0x57, 0xe2, |
||||||
|
0xc3, 0x2d, 0xaf, 0x21, 0xb1, 0xb3, 0x15, 0x81, 0x19, 0x26, 0x6b, 0x10, 0xb3, 0xe9, 0xd1, 0x45, |
||||||
|
0x21, 0x77, 0x9c, 0xf6, 0xe1, 0xdd, 0xb6, 0x78, 0x9d, 0x1d, 0x32, 0x61, 0xbc, 0x2b, 0xdb, 0x86, |
||||||
|
0xfb, 0x07, 0x24, 0x10, 0x19, 0x4f, 0x09, 0x6d, 0x03, 0x90, 0xd4, 0x5e, 0x30, 0x85, 0xc5, 0x58, |
||||||
|
0x7e, 0x5d, 0xae, 0x9f, 0x64, 0x93, 0x04, 0x82, 0x09, 0x0e, 0x1c, 0x66, 0xa8, 0x95, 0x91, 0x51, |
||||||
|
0xb2, 0xed, 0x9a, 0x75, 0x04, 0x87, 0x50, 0xac, 0xcc, 0x20, 0x06, 0x45, 0xb9, 0x7b, 0x42, 0x53, |
||||||
|
0x9a, 0xd1, 0x29, 0xfc, 0xef, 0xb9, 0x47, 0x16, 0x75, 0x69, 0x05, 0x87, 0x2b, 0xcb, 0x54, 0x9c, |
||||||
|
0x21, 0x2d, 0x50, 0x8e, 0x12, 0xde, 0xd3, 0x6b, 0xec, 0x92, 0xa1, 0xb1, 0xe9, 0x4b, 0xbf, 0x6b, |
||||||
|
0x9a, 0x38, 0xc7, 0x13, 0xfa, 0x78, 0xa1, 0x3c, 0x1e, 0xbb, 0x38, 0x31, 0xbb, 0x0c, 0x9f, 0x70, |
||||||
|
0x1a, 0x31, 0x00, 0xd7, 0x5a, 0xa5, 0x84, 0x24, 0x89, 0x80, 0xf5, 0x88, 0xc2, 0x31, 0x18, 0xdc, |
||||||
|
0x53, 0x05, 0x5d, 0xfa, 0x81, 0xdc, 0xe1, 0xce, 0xa4, 0xaa, 0xba, 0x07, 0xda, 0x28, 0x4f, 0x64, |
||||||
|
0x0e, 0x84, 0x9b, 0x06, 0xde, 0xc8, 0x78, 0x66, 0x2f, 0x17, 0x25, 0xa8, 0x9c, 0x99, 0xfc, 0xbc, |
||||||
|
0x7d, 0x01, 0x42, 0xd7, 0x35, 0xbf, 0x19, 0xf6, 0x3f, 0x20, 0xd9, 0x98, 0x9b, 0x5d, 0xdd, 0x39, |
||||||
|
0xbe, 0x81, 0x00, 0x0b, 0xde, 0x6f, 0x14, 0xca, 0x7e, 0xf8, 0xc0, 0x26, 0xa8, 0x1d, 0xd1, 0x16, |
||||||
|
0x88, 0x64, 0x87, 0x36, 0x45, 0x37, 0x50, 0xda, 0x6c, 0xeb, 0x85, 0xb5, 0x43, 0x29, 0x88, 0x6f, |
||||||
|
0x2f, 0xfe, 0x8d, 0x12, 0x8b, 0x72, 0xb7, 0x5a, 0xcb, 0x66, 0xc2, 0x2e, 0x1d, 0x7d, 0x42, 0xa6, |
||||||
|
0xf4, 0xfe, 0x26, 0x5d, 0x54, 0x9e, 0x77, 0x1d, 0x97, 0xc2, 0xf3, 0xfd, 0x60, 0xb3, 0x22, 0x88, |
||||||
|
0xca, 0x27, 0x99, 0xdf, 0xc8, 0xb1, 0xd7, 0xc6, 0x54, 0xa6, 0x50, 0xb9, 0x54, 0xf5, 0xde, 0xfe, |
||||||
|
0xe1, 0x81, 0xa2, 0xbe, 0x81, 0x9f, 0x48, 0xff, 0x2f, 0xb8, 0xa4, 0xb3, 0x17, 0xd8, 0xc1, 0xb9, |
||||||
|
0x5d, 0x21, 0x3d, 0xa2, 0xed, 0x1c, 0x96, 0x66, 0xee, 0x1f, 0x47, 0xcf, 0x62, 0xfa, 0xd6, 0xc1, |
||||||
|
0x87, 0x5b, 0xc4, 0xe5, 0xd9, 0x08, 0x38, 0x22, 0xfa, 0x21, 0xbd, 0xf2, 0x88, 0xda, 0xe2, 0x24, |
||||||
|
0x25, 0x1f, 0xf1, 0x0b, 0x2d, 0xae, 0x04, 0xbe, 0xa6, 0x7f, 0x75, 0x8c, 0xd9, 0x97, 0xe1, 0xca, |
||||||
|
0x35, 0xb9, 0xfc, 0x6f, 0x01, 0x68, 0x11, 0xd3, 0x68, 0x32, 0xd0, 0xc1, 0x69, 0xa3, 0xcf, 0x9b, |
||||||
|
0x10, 0xe4, 0x69, 0xa7, 0xcf, 0xe1, 0xfe, 0x2a, 0x07, 0x9e, 0xc1, 0x37, 0x84, 0x68, 0xe5, 0xc5, |
||||||
|
0xab, 0x25, 0xec, 0x7d, 0x7d, 0x74, 0x6a, 0xd1, 0xd5, 0x4d, 0xd7, 0xe1, 0x7d, 0xde, 0x30, 0x4b, |
||||||
|
0xe6, 0x5d, 0xcd, 0x91, 0x59, 0xf6, 0x80, 0xfd, 0xc6, 0x3c, 0xdd, 0x94, 0x7f, 0x15, 0x9d, 0xef, |
||||||
|
0x2f, 0x00, 0x62, 0xd7, 0xda, 0xb9, 0xb3, 0xd9, 0x8d, 0xe8, 0xd7, 0x3c, 0x96, 0x45, 0x5d, 0x1e, |
||||||
|
0x50, 0xfb, 0xaa, 0x43, 0xd3, 0x47, 0x77, 0x81, 0xe9, 0x67, 0xe4, 0xfe, 0xdf, 0x42, 0x79, 0xcb, |
||||||
|
0xa7, 0xad, 0x5d, 0x48, 0xf5, 0xb7, 0x74, 0x96, 0x12, 0x23, 0x06, 0x70, 0x42, 0x68, 0x7a, 0x44, |
||||||
|
0xfc, 0xa0, 0x31, 0x7f, 0x68, 0xca, 0xa2, 0x14, 0x5d, 0xa3, 0xcf, 0x42, 0x23, 0xab, 0x47, 0xf6, |
||||||
|
0xb2, 0xfc, 0x6d, 0xf1, |
||||||
|
]; |
||||||
|
|
||||||
|
/// Error decoding or verifying the encrypted physical store envelope.
|
||||||
|
#[derive(Debug, PartialEq, Eq)] |
||||||
|
pub enum CryptoError { |
||||||
|
/// Envelope shorter than its fixed 0x110-byte header.
|
||||||
|
Truncated, |
||||||
|
/// RSA signature over the encrypted AES key did not verify.
|
||||||
|
BadSignature, |
||||||
|
/// The physical store's HMAC/salted-SHA1 checksum did not match.
|
||||||
|
HmacMismatch, |
||||||
|
/// AES-CBC/PKCS7 decrypt failed (wrong key or corrupt data).
|
||||||
|
Aes, |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for CryptoError { |
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||||
|
match self { |
||||||
|
CryptoError::Truncated => write!(f, "physical store envelope truncated"), |
||||||
|
CryptoError::BadSignature => write!(f, "failed to decrypt physical store (bad RSA signature)"), |
||||||
|
CryptoError::HmacMismatch => write!(f, "failed to verify HMAC: physical store is corrupt"), |
||||||
|
CryptoError::Aes => write!(f, "AES-CBC decrypt failed"), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl std::error::Error for CryptoError {} |
||||||
|
|
||||||
|
/// Parse a CryptoAPI RSA `PRIVATEKEYBLOB` into an `RsaPrivateKey`.
|
||||||
|
///
|
||||||
|
/// Layout: `BLOBHEADER`(8) ++ `RSAPUBKEY`(magic "RSA2"(4), bitlen(4), pubexp(4))
|
||||||
|
/// ++ modulus(bitlen/8) ++ P ++ Q ++ Dp ++ Dq ++ Iq (each bitlen/16) ++
|
||||||
|
/// D(bitlen/8). Every integer field is little-endian, so each is reversed to
|
||||||
|
/// big-endian before constructing a `BigUint` — exactly what
|
||||||
|
/// `RSACryptoServiceProvider.ImportCspBlob` does internally.
|
||||||
|
fn parse_blob(blob: &[u8]) -> RsaPrivateKey { |
||||||
|
assert_eq!(&blob[8..12], b"RSA2", "not an RSA2 PRIVATEKEYBLOB"); |
||||||
|
let bitlen = u32::from_le_bytes(blob[12..16].try_into().unwrap()) as usize; |
||||||
|
let pubexp = u32::from_le_bytes(blob[16..20].try_into().unwrap()); |
||||||
|
let n_len = bitlen / 8; // modulus / D
|
||||||
|
let h_len = bitlen / 16; // P / Q / Dp / Dq / Iq
|
||||||
|
|
||||||
|
let mut off = 20usize; |
||||||
|
let mut field = |len: usize| -> BigUint { |
||||||
|
let mut b = blob[off..off + len].to_vec(); |
||||||
|
off += len; |
||||||
|
b.reverse(); // little-endian -> big-endian
|
||||||
|
BigUint::from_bytes_be(&b) |
||||||
|
}; |
||||||
|
|
||||||
|
let n = field(n_len); |
||||||
|
let p = field(h_len); |
||||||
|
let q = field(h_len); |
||||||
|
let _dp = field(h_len); |
||||||
|
let _dq = field(h_len); |
||||||
|
let _iq = field(h_len); |
||||||
|
let d = field(n_len); |
||||||
|
|
||||||
|
RsaPrivateKey::from_components(n, BigUint::from(pubexp), d, vec![p, q]) |
||||||
|
.expect("valid CryptoAPI PRIVATEKEYBLOB") |
||||||
|
} |
||||||
|
|
||||||
|
fn sha1_hash(data: &[u8]) -> [u8; 20] { |
||||||
|
let mut h = Sha1::new(); |
||||||
|
h.update(data); |
||||||
|
h.finalize().into() |
||||||
|
} |
||||||
|
|
||||||
|
/// `CryptoUtils.SaltSHASum`: `SHA1(salt ++ data)` (Vista physical-store checksum).
|
||||||
|
fn salt_sha1(salt: &[u8], data: &[u8]) -> [u8; 20] { |
||||||
|
let mut h = Sha1::new(); |
||||||
|
h.update(salt); |
||||||
|
h.update(data); |
||||||
|
h.finalize().into() |
||||||
|
} |
||||||
|
|
||||||
|
/// `CryptoUtils.AESEncrypt`: AES-128-CBC/PKCS7, zero IV.
|
||||||
|
fn aes_encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Vec<u8> { |
||||||
|
Aes128CbcEnc::new_from_slices(key, iv) |
||||||
|
.expect("AES-128 key/iv length") |
||||||
|
.encrypt_padded_vec_mut::<Pkcs7>(data) |
||||||
|
} |
||||||
|
|
||||||
|
/// `CryptoUtils.AESDecrypt`: AES-128-CBC/PKCS7, zero IV.
|
||||||
|
fn aes_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>, CryptoError> { |
||||||
|
Aes128CbcDec::new_from_slices(key, iv) |
||||||
|
.map_err(|_| CryptoError::Aes)? |
||||||
|
.decrypt_padded_vec_mut::<Pkcs7>(data) |
||||||
|
.map_err(|_| CryptoError::Aes) |
||||||
|
} |
||||||
|
|
||||||
|
/// The RSA/AES/HMAC backend behind a signed trusted-store ticket.
|
||||||
|
///
|
||||||
|
/// Holds the parsed SPP trusted-store RSA key (production or test). Construct
|
||||||
|
/// with [`RealCrypto::production`] / [`RealCrypto::test`].
|
||||||
|
pub struct RealCrypto { |
||||||
|
priv_key: RsaPrivateKey, |
||||||
|
pub_key: RsaPublicKey, |
||||||
|
} |
||||||
|
|
||||||
|
impl RealCrypto { |
||||||
|
/// Build from a raw CryptoAPI `PRIVATEKEYBLOB`.
|
||||||
|
pub fn from_blob(blob: &[u8]) -> Self { |
||||||
|
let priv_key = parse_blob(blob); |
||||||
|
let pub_key = RsaPublicKey::from(&priv_key); |
||||||
|
RealCrypto { priv_key, pub_key } |
||||||
|
} |
||||||
|
|
||||||
|
/// Backend using the production trusted-store key ([`PRODUCTION`]).
|
||||||
|
pub fn production() -> Self { |
||||||
|
Self::from_blob(&PRODUCTION) |
||||||
|
} |
||||||
|
|
||||||
|
/// Backend using the test trusted-store key ([`TEST`]).
|
||||||
|
pub fn test() -> Self { |
||||||
|
Self::from_blob(&TEST) |
||||||
|
} |
||||||
|
|
||||||
|
/// `CryptoUtils.RSAEncrypt`: RSA PKCS#1 v1.5 encrypt with the public key.
|
||||||
|
fn rsa_encrypt(&self, data: &[u8]) -> Vec<u8> { |
||||||
|
let mut rng = OsRng; |
||||||
|
self.pub_key |
||||||
|
.encrypt(&mut rng, Pkcs1v15Encrypt, data) |
||||||
|
.expect("RSA encrypt") |
||||||
|
} |
||||||
|
|
||||||
|
/// `CryptoUtils.RSADecrypt`: RSA PKCS#1 v1.5 decrypt with the private key.
|
||||||
|
fn rsa_decrypt(&self, data: &[u8]) -> Result<Vec<u8>, CryptoError> { |
||||||
|
self.priv_key |
||||||
|
.decrypt(Pkcs1v15Encrypt, data) |
||||||
|
.map_err(|_| CryptoError::BadSignature) |
||||||
|
} |
||||||
|
|
||||||
|
/// `CryptoUtils.RSAVerifySignature`: PKCS#1 v1.5 SHA1 signature over `data`.
|
||||||
|
fn rsa_verify(&self, data: &[u8], sig: &[u8]) -> bool { |
||||||
|
let hash = sha1_hash(data); |
||||||
|
self.pub_key |
||||||
|
.verify(Pkcs1v15Sign::new::<Sha1>(), &hash, sig) |
||||||
|
.is_ok() |
||||||
|
} |
||||||
|
|
||||||
|
/// `PhysStoreCrypto.EncryptPhysicalStore`: wrap `data` in the signed,
|
||||||
|
/// AES-encrypted `data.dat` "UNTRUSTSTORE" envelope for `version`.
|
||||||
|
pub fn encrypt_physical_store(&self, data: &[u8], version: PsVersion) -> Vec<u8> { |
||||||
|
let aes_key = AES_KEY; |
||||||
|
let mut hmac_key = [0u8; 0x10]; |
||||||
|
OsRng.fill_bytes(&mut hmac_key); |
||||||
|
|
||||||
|
let enc_aes_key = self.rsa_encrypt(aes_key); // 0x80
|
||||||
|
let aes_key_sig = self.rsa_sign(&sha1_hash(&enc_aes_key)); // 0x80
|
||||||
|
|
||||||
|
let hmac_sig: [u8; 20] = if version != PsVersion::Vista { |
||||||
|
self.hmac_sha1(&hmac_key, data) |
||||||
|
} else { |
||||||
|
salt_sha1(&hmac_key, data) |
||||||
|
}; |
||||||
|
|
||||||
|
// decData = hmacKey(0x10) ++ hmacSig(0x14) ++ int32(0) ++ psData
|
||||||
|
let mut dec = Vec::with_capacity(0x28 + data.len()); |
||||||
|
dec.extend_from_slice(&hmac_key); |
||||||
|
dec.extend_from_slice(&hmac_sig); |
||||||
|
dec.extend_from_slice(&0u32.to_le_bytes()); |
||||||
|
dec.extend_from_slice(data); |
||||||
|
let enc_data = aes_encrypt(aes_key, &[0u8; 16], &dec); |
||||||
|
|
||||||
|
// version(4) ++ "UNTRUSTSTORE"(12) ++ aesKeySig(0x80) ++ encAesKey(0x80) ++ AES(...)
|
||||||
|
let mut out = Vec::with_capacity(0x110 + enc_data.len()); |
||||||
|
out.extend_from_slice(&version.envelope_version().to_le_bytes()); |
||||||
|
out.extend_from_slice(b"UNTRUSTSTORE"); |
||||||
|
out.extend_from_slice(&aes_key_sig); |
||||||
|
out.extend_from_slice(&enc_aes_key); |
||||||
|
out.extend_from_slice(&enc_data); |
||||||
|
out |
||||||
|
} |
||||||
|
|
||||||
|
/// `PhysStoreCrypto.DecryptPhysicalStore`: verify the RSA signature and
|
||||||
|
/// HMAC/salted-SHA1 checksum, then return the inner physical-store bytes.
|
||||||
|
pub fn decrypt_physical_store( |
||||||
|
&self, |
||||||
|
data: &[u8], |
||||||
|
version: PsVersion, |
||||||
|
) -> Result<Vec<u8>, CryptoError> { |
||||||
|
if data.len() < 0x110 { |
||||||
|
return Err(CryptoError::Truncated); |
||||||
|
} |
||||||
|
// Skip version(4) + "UNTRUSTSTORE"(12) = 0x10.
|
||||||
|
let aes_key_sig = &data[0x10..0x90]; |
||||||
|
let enc_aes_key = &data[0x90..0x110]; |
||||||
|
let ciphertext = &data[0x110..]; |
||||||
|
|
||||||
|
if !self.rsa_verify(enc_aes_key, aes_key_sig) { |
||||||
|
return Err(CryptoError::BadSignature); |
||||||
|
} |
||||||
|
|
||||||
|
let aes_key = self.rsa_decrypt(enc_aes_key)?; |
||||||
|
let dec = aes_decrypt(&aes_key, &[0u8; 16], ciphertext)?; |
||||||
|
if dec.len() < 0x28 { |
||||||
|
return Err(CryptoError::Truncated); |
||||||
|
} |
||||||
|
let hmac_key = &dec[0x00..0x10]; |
||||||
|
let hmac_sig = &dec[0x10..0x24]; // 0x14 bytes
|
||||||
|
let ps_data = &dec[0x28..]; // int32(0) at 0x24..0x28 is skipped
|
||||||
|
|
||||||
|
let expected: [u8; 20] = if version != PsVersion::Vista { |
||||||
|
self.hmac_sha1(hmac_key, ps_data) |
||||||
|
} else { |
||||||
|
salt_sha1(hmac_key, ps_data) |
||||||
|
}; |
||||||
|
if expected[..] != *hmac_sig { |
||||||
|
return Err(CryptoError::HmacMismatch); |
||||||
|
} |
||||||
|
Ok(ps_data.to_vec()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TicketCrypto for RealCrypto { |
||||||
|
fn hmac_sha1(&self, key: &[u8], data: &[u8]) -> [u8; 20] { |
||||||
|
let mut mac = <HmacSha1 as Mac>::new_from_slice(key).expect("HMAC accepts any key length"); |
||||||
|
mac.update(data); |
||||||
|
mac.finalize().into_bytes().into() |
||||||
|
} |
||||||
|
|
||||||
|
fn aes_cbc_decrypt(&self, key: &[u8], iv: &[u8], data: &[u8]) -> Vec<u8> { |
||||||
|
aes_decrypt(key, iv, data).expect("AES-CBC decrypt") |
||||||
|
} |
||||||
|
|
||||||
|
/// `CryptoUtils.RSASign`: PKCS#1 v1.5 SHA1 signature over an already-computed
|
||||||
|
/// 20-byte SHA1 `digest`. The SHA1 `DigestInfo` prefix comes from the
|
||||||
|
/// `oid` feature of the `sha1` crate, matching .NET's
|
||||||
|
/// `RSAPKCS1SignatureFormatter.SetHashAlgorithm("SHA1")`.
|
||||||
|
fn rsa_sign(&self, digest: &[u8]) -> Vec<u8> { |
||||||
|
self.priv_key |
||||||
|
.sign(Pkcs1v15Sign::new::<Sha1>(), digest) |
||||||
|
.expect("RSA sign") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn key_blobs_are_1024_bit() { |
||||||
|
// A 1024-bit modulus yields 0x80-byte signatures/ciphertext.
|
||||||
|
for c in [RealCrypto::production(), RealCrypto::test()] { |
||||||
|
assert_eq!(c.rsa_sign(&sha1_hash(b"x")).len(), 0x80); |
||||||
|
assert_eq!(c.rsa_encrypt(AES_KEY).len(), 0x80); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn rsa_sign_verify_round_trips() { |
||||||
|
let c = RealCrypto::test(); |
||||||
|
let digest = sha1_hash(b"encrypted aes key bytes"); |
||||||
|
let sig = c.rsa_sign(&digest); |
||||||
|
assert_eq!(sig.len(), 0x80); |
||||||
|
assert!(c |
||||||
|
.pub_key |
||||||
|
.verify(Pkcs1v15Sign::new::<Sha1>(), &digest, &sig) |
||||||
|
.is_ok()); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn rsa_encrypt_decrypt_round_trips() { |
||||||
|
let c = RealCrypto::test(); |
||||||
|
let ct = c.rsa_encrypt(AES_KEY); |
||||||
|
assert_eq!(ct.len(), 0x80); |
||||||
|
assert_eq!(c.rsa_decrypt(&ct).unwrap(), AES_KEY); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn salt_sha1_known_vector() { |
||||||
|
// SHA1(salt ++ data) with empty salt == SHA1(data): "abc" digest.
|
||||||
|
assert_eq!( |
||||||
|
salt_sha1(b"", b"abc"), |
||||||
|
[ |
||||||
|
0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50, |
||||||
|
0xc2, 0x6c, 0x9c, 0xd0, 0xd8, 0x9d |
||||||
|
] |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn envelope_round_trips_all_dialects() { |
||||||
|
let data = b"physical store payload bytes \x00\x01\x02".to_vec(); |
||||||
|
for prod in [true, false] { |
||||||
|
let c = if prod { RealCrypto::production() } else { RealCrypto::test() }; |
||||||
|
for v in [PsVersion::Vista, PsVersion::Win7, PsVersion::Win8, PsVersion::WinModern] { |
||||||
|
let env = c.encrypt_physical_store(&data, v); |
||||||
|
assert_eq!(&env[4..16], b"UNTRUSTSTORE"); |
||||||
|
assert_eq!( |
||||||
|
u32::from_le_bytes(env[0..4].try_into().unwrap()), |
||||||
|
v.envelope_version() |
||||||
|
); |
||||||
|
assert_eq!(c.decrypt_physical_store(&env, v).unwrap(), data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn tampered_signature_rejected() { |
||||||
|
let c = RealCrypto::test(); |
||||||
|
let mut env = c.encrypt_physical_store(b"data", PsVersion::WinModern); |
||||||
|
env[0x10] ^= 0xFF; // corrupt first byte of aesKeySig
|
||||||
|
assert_eq!( |
||||||
|
c.decrypt_physical_store(&env, PsVersion::WinModern), |
||||||
|
Err(CryptoError::BadSignature) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn tampered_payload_fails_hmac() { |
||||||
|
let c = RealCrypto::test(); |
||||||
|
let env = c.encrypt_physical_store(b"original payload", PsVersion::WinModern); |
||||||
|
let mut env2 = env.clone(); |
||||||
|
let last = env2.len() - 1; |
||||||
|
env2[last] ^= 0xFF; // corrupt ciphertext -> AES/HMAC failure
|
||||||
|
assert!(c.decrypt_physical_store(&env2, PsVersion::WinModern).is_err()); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn truncated_envelope_rejected() { |
||||||
|
let c = RealCrypto::test(); |
||||||
|
assert_eq!( |
||||||
|
c.decrypt_physical_store(&[0u8; 4], PsVersion::WinModern), |
||||||
|
Err(CryptoError::Truncated) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,419 @@ |
|||||||
|
//! Product-key packing/unpacking and PID helpers, ported from LibTSforge
|
||||||
|
//! `ProductKey.cs`.
|
||||||
|
//!
|
||||||
|
//! A PKEY2009 product key is a 114-bit value packed into two `u64`s
|
||||||
|
//! (`klow`/`khigh`) carrying the group, serial, security value, an upgrade
|
||||||
|
//! flag, and a 10-bit CRC-32 checksum, then rendered as the familiar
|
||||||
|
//! 25-character base-24 string (charset `BCDFGHJKMPQRTVWXY2346789`) with an
|
||||||
|
//! `N` marker inserted at a position that itself encodes the top digit.
|
||||||
|
//!
|
||||||
|
//! This module is pure (std-only): everything the C# reads from the
|
||||||
|
//! environment (OS build, LCID, date, the non-deterministic PID randomiser) is
|
||||||
|
//! passed in as a parameter so the logic round-trips under `cargo test`.
|
||||||
|
//!
|
||||||
|
//! Intentionally NOT ported (out of scope / not pure):
|
||||||
|
//! * `GetPkeyId`, `GetPhoneData`, `GetAlgoUri` — belong to the variable-bag /
|
||||||
|
//! crypto layers.
|
||||||
|
//! * the PKEY2005 `ToString` branch — it draws from .NET's seeded `Random`; the
|
||||||
|
//! keys it makes are placeholders, and reproducing that PRNG is out of scope.
|
||||||
|
//! * the `setup.cfg` MPC override in `GetMPC` — file I/O; only the build table
|
||||||
|
//! is ported (that is the branch TSforge actually relies on).
|
||||||
|
|
||||||
|
use crate::common::encode_utf16; |
||||||
|
use crate::crc32::crc32; |
||||||
|
|
||||||
|
/// Base-24 charset (`ProductKey.ALPHABET`). Note: no `N` — the `N` marker in a
|
||||||
|
/// rendered key is therefore unambiguous.
|
||||||
|
const ALPHABET: &[u8; 24] = b"BCDFGHJKMPQRTVWXY2346789"; |
||||||
|
|
||||||
|
/// `PKeyAlgorithm`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||||
|
pub enum PKeyAlgorithm { |
||||||
|
Pkey2005, |
||||||
|
Pkey2009, |
||||||
|
} |
||||||
|
|
||||||
|
/// A decoded product key plus the config-derived metadata the PID builders need.
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub struct ProductKey { |
||||||
|
pub group: u32, |
||||||
|
pub serial: u32, |
||||||
|
pub security: u64, |
||||||
|
pub upgrade: bool, |
||||||
|
pub algorithm: PKeyAlgorithm, |
||||||
|
pub eula_type: String, |
||||||
|
pub part_number: String, |
||||||
|
pub edition: String, |
||||||
|
pub channel: String, |
||||||
|
/// `ActivationId.ToString()` — the config GUID, formatted `d`.
|
||||||
|
pub activation_id: String, |
||||||
|
klow: u64, |
||||||
|
khigh: u64, |
||||||
|
} |
||||||
|
|
||||||
|
/// `GetMPC` build table (the file-override branch is not ported).
|
||||||
|
pub fn get_mpc(build: u32) -> &'static str { |
||||||
|
if build >= 10240 { |
||||||
|
"03612" |
||||||
|
} else if build >= 9600 { |
||||||
|
"06401" |
||||||
|
} else if build >= 9200 { |
||||||
|
"05426" |
||||||
|
} else { |
||||||
|
"55041" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Pack `(group, serial, security, upgrade)` into `(klow, khigh)`, writing the
|
||||||
|
/// 10-bit CRC-32 checksum into `khigh` bits 39..=48 exactly as the C# ctor does.
|
||||||
|
pub fn pack_key(group: u32, serial: u32, security: u64, upgrade: bool) -> (u64, u64) { |
||||||
|
let klow = ((security & 0x3fff) << 50) |
||||||
|
| (((serial as u64) & 0x3fff_ffff) << 20) |
||||||
|
| ((group as u64) & 0xf_ffff); |
||||||
|
let mut khigh = ((upgrade as u64) << 49) | ((security >> 14) & 0x7f_ffff_ffff); |
||||||
|
|
||||||
|
// Checksum is computed over the key bytes *before* it is folded in.
|
||||||
|
let mut kb = [0u8; 16]; |
||||||
|
kb[..8].copy_from_slice(&klow.to_le_bytes()); |
||||||
|
kb[8..].copy_from_slice(&khigh.to_le_bytes()); |
||||||
|
let checksum = (crc32(&kb) & 0x3ff) as u64; |
||||||
|
|
||||||
|
khigh |= checksum << 39; |
||||||
|
(klow, khigh) |
||||||
|
} |
||||||
|
|
||||||
|
/// Inverse of [`pack_key`]. Returns `(group, serial, security, upgrade, checksum)`.
|
||||||
|
pub fn unpack_key(klow: u64, khigh: u64) -> (u32, u32, u64, bool, u32) { |
||||||
|
let group = (klow & 0xf_ffff) as u32; |
||||||
|
let serial = ((klow >> 20) & 0x3fff_ffff) as u32; |
||||||
|
let sec_low = (klow >> 50) & 0x3fff; |
||||||
|
let sec_high = khigh & 0x7f_ffff_ffff; |
||||||
|
let security = (sec_high << 14) | sec_low; |
||||||
|
let upgrade = (khigh >> 49) & 1 == 1; |
||||||
|
let checksum = ((khigh >> 39) & 0x3ff) as u32; |
||||||
|
(group, serial, security, upgrade, checksum) |
||||||
|
} |
||||||
|
|
||||||
|
/// Little-endian 15-byte `acc *= 24; acc += d` (top byte stays 0).
|
||||||
|
fn mul_add_24(acc: &mut [u8; 16], d: u32) { |
||||||
|
let mut carry = d; |
||||||
|
for k in 0..15 { |
||||||
|
let v = acc[k] as u32 * 24 + carry; |
||||||
|
acc[k] = (v & 0xff) as u8; |
||||||
|
carry = v >> 8; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Base-24 encode a 16-byte key into the dashed 25-char string (PKEY2009 form).
|
||||||
|
pub fn base24_encode(key: &[u8; 16]) -> String { |
||||||
|
let mut b = *key; |
||||||
|
let mut chars: Vec<u8> = Vec::with_capacity(25); |
||||||
|
let mut last = 0usize; |
||||||
|
|
||||||
|
for _ in 0..25 { |
||||||
|
let mut current = 0u32; |
||||||
|
for j in (0..15).rev() { |
||||||
|
current = current * 0x100 + b[j] as u32; |
||||||
|
b[j] = (current / 24) as u8; |
||||||
|
current %= 24; |
||||||
|
} |
||||||
|
last = current as usize; |
||||||
|
chars.insert(0, ALPHABET[current as usize]); |
||||||
|
} |
||||||
|
|
||||||
|
// Drop char 0 (its value is `last`) and reinsert it as the position of `N`.
|
||||||
|
let mut out: Vec<u8> = Vec::with_capacity(25); |
||||||
|
out.extend_from_slice(&chars[1..1 + last]); |
||||||
|
out.push(b'N'); |
||||||
|
out.extend_from_slice(&chars[last + 1..]); |
||||||
|
|
||||||
|
out.chunks(5) |
||||||
|
.map(|c| c.iter().map(|&x| x as char).collect::<String>()) |
||||||
|
.collect::<Vec<_>>() |
||||||
|
.join("-") |
||||||
|
} |
||||||
|
|
||||||
|
/// Inverse of [`base24_encode`]: dashed key string -> 16-byte key.
|
||||||
|
pub fn base24_decode(s: &str) -> [u8; 16] { |
||||||
|
let clean: Vec<u8> = s.bytes().filter(|&x| x != b'-').collect(); |
||||||
|
let last = clean |
||||||
|
.iter() |
||||||
|
.position(|&x| x == b'N') |
||||||
|
.expect("product key has no 'N' marker"); |
||||||
|
|
||||||
|
let mut acc = [0u8; 16]; |
||||||
|
// digits, most-significant first: [last] then every non-'N' char in order.
|
||||||
|
mul_add_24(&mut acc, last as u32); |
||||||
|
for (i, &c) in clean.iter().enumerate() { |
||||||
|
if i == last { |
||||||
|
continue; |
||||||
|
} |
||||||
|
let d = ALPHABET |
||||||
|
.iter() |
||||||
|
.position(|&a| a == c) |
||||||
|
.expect("invalid base-24 character") as u32; |
||||||
|
mul_add_24(&mut acc, d); |
||||||
|
} |
||||||
|
acc |
||||||
|
} |
||||||
|
|
||||||
|
impl ProductKey { |
||||||
|
/// Mirror of the C# constructor (minus the config/range plumbing): pack the
|
||||||
|
/// key and stash the metadata the PID builders reference.
|
||||||
|
#[allow(clippy::too_many_arguments)] |
||||||
|
pub fn new( |
||||||
|
group: u32, |
||||||
|
serial: u32, |
||||||
|
security: u64, |
||||||
|
upgrade: bool, |
||||||
|
algorithm: PKeyAlgorithm, |
||||||
|
eula_type: impl Into<String>, |
||||||
|
part_number: impl Into<String>, |
||||||
|
edition: impl Into<String>, |
||||||
|
channel: impl Into<String>, |
||||||
|
activation_id: impl Into<String>, |
||||||
|
) -> Self { |
||||||
|
let (klow, khigh) = pack_key(group, serial, security, upgrade); |
||||||
|
ProductKey { |
||||||
|
group, |
||||||
|
serial, |
||||||
|
security, |
||||||
|
upgrade, |
||||||
|
algorithm, |
||||||
|
eula_type: eula_type.into(), |
||||||
|
part_number: part_number.into(), |
||||||
|
edition: edition.into(), |
||||||
|
channel: channel.into(), |
||||||
|
activation_id: activation_id.into(), |
||||||
|
klow, |
||||||
|
khigh, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `KeyBytes` = `klow` ‖ `khigh`, both little-endian (16 bytes).
|
||||||
|
pub fn key_bytes(&self) -> [u8; 16] { |
||||||
|
let mut kb = [0u8; 16]; |
||||||
|
kb[..8].copy_from_slice(&self.klow.to_le_bytes()); |
||||||
|
kb[8..].copy_from_slice(&self.khigh.to_le_bytes()); |
||||||
|
kb |
||||||
|
} |
||||||
|
|
||||||
|
/// The 10-bit checksum packed into `khigh`.
|
||||||
|
pub fn checksum(&self) -> u32 { |
||||||
|
((self.khigh >> 39) & 0x3ff) as u32 |
||||||
|
} |
||||||
|
|
||||||
|
/// Rendered PKEY2009 product-key string.
|
||||||
|
pub fn to_key_string(&self) -> String { |
||||||
|
debug_assert_eq!(self.algorithm, PKeyAlgorithm::Pkey2009); |
||||||
|
base24_encode(&self.key_bytes()) |
||||||
|
} |
||||||
|
|
||||||
|
/// `GetPid2` (only PKEY2005 produces a value; PKEY2009 returns `""`).
|
||||||
|
/// `rand_1000` supplies the `Random().Next(1000)` term of the non-OEM path.
|
||||||
|
pub fn get_pid2(&self, build: u32, rand_1000: u32) -> String { |
||||||
|
if self.algorithm != PKeyAlgorithm::Pkey2005 { |
||||||
|
return String::new(); |
||||||
|
} |
||||||
|
let mpc = get_mpc(build); |
||||||
|
let (serial_high, serial_low, last_part): (String, u32, u32) = if self.eula_type == "OEM" { |
||||||
|
( |
||||||
|
"OEM".to_string(), |
||||||
|
(self.group / 2 % 100) * 10000 + self.serial / 100000, |
||||||
|
self.serial % 100000, |
||||||
|
) |
||||||
|
} else { |
||||||
|
( |
||||||
|
format!("{:03}", self.serial / 1000000), |
||||||
|
self.serial % 1000000, |
||||||
|
(self.group / 2 % 100) * 1000 + rand_1000, |
||||||
|
) |
||||||
|
}; |
||||||
|
|
||||||
|
let digit_sum: u32 = serial_low |
||||||
|
.to_string() |
||||||
|
.bytes() |
||||||
|
.map(|b| (b - b'0') as u32) |
||||||
|
.sum(); |
||||||
|
let checksum = 7 - (digit_sum % 7); |
||||||
|
|
||||||
|
format!( |
||||||
|
"{}-{}-{:06}{}-{:05}", |
||||||
|
mpc, serial_high, serial_low, checksum, last_part |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// `GetPid3`.
|
||||||
|
pub fn get_pid3(&self, build: u32, rand_1000: u32) -> Vec<u8> { |
||||||
|
let mut out: Vec<u8> = Vec::new(); |
||||||
|
out.extend_from_slice(&0xA4u32.to_le_bytes()); |
||||||
|
out.extend_from_slice(&0x3u32.to_le_bytes()); |
||||||
|
write_fixed_ascii(&mut out, &self.get_pid2(build, rand_1000), 24); |
||||||
|
out.extend_from_slice(&self.group.to_le_bytes()); |
||||||
|
write_fixed_ascii(&mut out, &self.part_number, 16); |
||||||
|
out.extend(std::iter::repeat(0u8).take(0x6C)); |
||||||
|
|
||||||
|
let mut rev = out.clone(); |
||||||
|
rev.reverse(); |
||||||
|
let mut crc = (!crc32(&rev)).to_le_bytes(); |
||||||
|
crc.reverse(); |
||||||
|
out.extend_from_slice(&crc); |
||||||
|
out |
||||||
|
} |
||||||
|
|
||||||
|
/// `GetExtendedPid`.
|
||||||
|
pub fn get_extended_pid(&self, build: u32, lcid: u32, day_of_year: u32, year: u32) -> String { |
||||||
|
let mpc = get_mpc(build); |
||||||
|
let serial_high = self.serial / 1000000; |
||||||
|
let serial_low = self.serial % 1000000; |
||||||
|
let license_type = match self.eula_type.as_str() { |
||||||
|
"OEM" => 2, |
||||||
|
"Volume" => 3, |
||||||
|
_ => 0, |
||||||
|
}; |
||||||
|
format!( |
||||||
|
"{}-{:05}-{:03}-{:06}-{:02}-{:04}-{:04}.0000-{:03}{:04}", |
||||||
|
mpc, self.group, serial_high, serial_low, license_type, lcid, build, day_of_year, year |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// `GetPid4`.
|
||||||
|
pub fn get_pid4(&self, build: u32, lcid: u32, day_of_year: u32, year: u32) -> Vec<u8> { |
||||||
|
let mut out: Vec<u8> = Vec::new(); |
||||||
|
out.extend_from_slice(&0x4F8u32.to_le_bytes()); |
||||||
|
out.extend_from_slice(&0x4u32.to_le_bytes()); |
||||||
|
write_fixed_utf16( |
||||||
|
&mut out, |
||||||
|
&self.get_extended_pid(build, lcid, day_of_year, year), |
||||||
|
0x80, |
||||||
|
); |
||||||
|
write_fixed_utf16(&mut out, &self.activation_id, 0x80); |
||||||
|
out.extend(std::iter::repeat(0u8).take(0x10)); |
||||||
|
write_fixed_utf16(&mut out, &self.edition, 0x208); |
||||||
|
out.extend_from_slice(&(self.upgrade as u64).to_le_bytes()); |
||||||
|
out.extend(std::iter::repeat(0u8).take(0x50)); |
||||||
|
write_fixed_utf16(&mut out, &self.part_number, 0x80); |
||||||
|
write_fixed_utf16(&mut out, &self.channel, 0x80); |
||||||
|
write_fixed_utf16(&mut out, &self.eula_type, 0x80); |
||||||
|
out |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `WriteFixedString`: ASCII bytes zero-padded to `blen`.
|
||||||
|
fn write_fixed_ascii(out: &mut Vec<u8>, s: &str, blen: usize) { |
||||||
|
out.extend_from_slice(s.as_bytes()); |
||||||
|
out.extend(std::iter::repeat(0u8).take(blen - s.len())); |
||||||
|
} |
||||||
|
|
||||||
|
/// `WriteFixedString16`: UTF-16LE + NUL, zero-padded to `blen`.
|
||||||
|
fn write_fixed_utf16(out: &mut Vec<u8>, s: &str, blen: usize) { |
||||||
|
let enc = encode_utf16(s); |
||||||
|
out.extend_from_slice(&enc); |
||||||
|
out.extend(std::iter::repeat(0u8).take(blen - enc.len())); |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
fn sample_2009() -> ProductKey { |
||||||
|
ProductKey::new( |
||||||
|
2265, // group
|
||||||
|
123456789, // serial
|
||||||
|
0x1_2345, // security
|
||||||
|
true, // upgrade
|
||||||
|
PKeyAlgorithm::Pkey2009, |
||||||
|
"OEM", |
||||||
|
"X19-98765", |
||||||
|
"Professional", |
||||||
|
"Retail", |
||||||
|
"12345678-1234-1234-1234-123456789012", |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn base24_round_trips() { |
||||||
|
let pk = sample_2009(); |
||||||
|
let s = pk.to_key_string(); |
||||||
|
|
||||||
|
// Rendered shape: 5 groups of 5, exactly one 'N', charset-clean.
|
||||||
|
assert_eq!(s.len(), 29); // 25 chars + 4 dashes
|
||||||
|
assert_eq!(s.matches('N').count(), 1); |
||||||
|
assert!(s |
||||||
|
.bytes() |
||||||
|
.all(|b| b == b'-' || b == b'N' || ALPHABET.contains(&b))); |
||||||
|
|
||||||
|
assert_eq!(base24_decode(&s), pk.key_bytes()); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn base24_decode_ignores_grouping() { |
||||||
|
let pk = sample_2009(); |
||||||
|
let dashed = pk.to_key_string(); |
||||||
|
let undashed: String = dashed.chars().filter(|&c| c != '-').collect(); |
||||||
|
assert_eq!(base24_decode(&dashed), base24_decode(&undashed)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn pack_unpack_round_trips() { |
||||||
|
let (klow, khigh) = pack_key(2265, 123456789, 0x1_2345, true); |
||||||
|
let (g, s, sec, up, _cs) = unpack_key(klow, khigh); |
||||||
|
assert_eq!((g, s, sec, up), (2265, 123456789, 0x1_2345, true)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn checksum_lands_in_khigh_bits_39_to_48() { |
||||||
|
let pk = sample_2009(); |
||||||
|
let kb = pk.key_bytes(); |
||||||
|
let khigh = u64::from_le_bytes(kb[8..].try_into().unwrap()); |
||||||
|
let field = ((khigh >> 39) & 0x3ff) as u32; |
||||||
|
|
||||||
|
// Recompute over the key with the checksum field zeroed.
|
||||||
|
let khigh_no_cs = khigh & !(0x3ffu64 << 39); |
||||||
|
let mut kb2 = kb; |
||||||
|
kb2[8..].copy_from_slice(&khigh_no_cs.to_le_bytes()); |
||||||
|
assert_eq!(field, crc32(&kb2) & 0x3ff); |
||||||
|
assert_eq!(field, pk.checksum()); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn mpc_by_build() { |
||||||
|
assert_eq!(get_mpc(19045), "03612"); |
||||||
|
assert_eq!(get_mpc(10240), "03612"); |
||||||
|
assert_eq!(get_mpc(10239), "06401"); |
||||||
|
assert_eq!(get_mpc(9600), "06401"); |
||||||
|
assert_eq!(get_mpc(9200), "05426"); |
||||||
|
assert_eq!(get_mpc(7601), "55041"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn pid_blobs_have_the_expected_fixed_sizes() { |
||||||
|
let pk = sample_2009(); |
||||||
|
// 4 + 4 + 24 + 4 + 16 + 108 + 4(crc)
|
||||||
|
assert_eq!(pk.get_pid3(19045, 0).len(), 164); |
||||||
|
// header value 0x4F8 == total length.
|
||||||
|
assert_eq!(pk.get_pid4(19045, 1033, 210, 2026).len(), 0x4F8); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn pid2_oem_checksum_and_shape() { |
||||||
|
// A PKEY2005 OEM key exercises the deterministic PID2 path.
|
||||||
|
let pk = ProductKey::new( |
||||||
|
100, |
||||||
|
250123, |
||||||
|
0, |
||||||
|
false, |
||||||
|
PKeyAlgorithm::Pkey2005, |
||||||
|
"OEM", |
||||||
|
"PN", |
||||||
|
"Core", |
||||||
|
"OEM", |
||||||
|
"id", |
||||||
|
); |
||||||
|
let pid2 = pk.get_pid2(9200, 0); |
||||||
|
// mpc-OEM-{serialLow:06}{chk}-{lastPart:05}
|
||||||
|
// serialLow = (100/2 % 100)*10000 + 250123/100000 = 500000 + 2 = 500002
|
||||||
|
// digitsum(500002)=7 -> chk = 7 - 0 = 7 ; lastPart = 250123 % 100000 = 50123
|
||||||
|
assert_eq!(pid2, "05426-OEM-5000027-50123"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,413 @@ |
|||||||
|
//! Verbatim product tables transcribed from
|
||||||
|
//! `Ohook_Activation_AIO.cmd` (`:ohookdata` and `:msiofficedata`), MAS 3.12.
|
||||||
|
//!
|
||||||
|
//! Kept as raw strings and parsed at runtime by [`super`] so the transcription
|
||||||
|
//! stays byte-faithful to the script (including the `%f%` empty-variable
|
||||||
|
//! splitter, which the parser strips from keys). `::`-prefixed lines are the
|
||||||
|
//! script's own version headers and are skipped by the parser.
|
||||||
|
//!
|
||||||
|
//! Counts: OHOOK_TABLE = 235 rows (14: 32, 15: 52, 16: 151).
|
||||||
|
//! MSI_TABLE = 148 rows (14: 43, 15: 52, 16: 53).
|
||||||
|
|
||||||
|
/// `:ohookdata` — `ver_activationId_key(%f%)_licenseSuffix_edition[_otherIds]`.
|
||||||
|
pub const OHOOK_TABLE: &str = r#" |
||||||
|
:: Office 2010 |
||||||
|
14_4d463c2c-0505-4626-8cdb-a4da82e2d8ed_7KTYC-XR43P-C3MRW-BJKFD-XB%f%YPG_Retail________AccessR |
||||||
|
14_745fb377-0a59-4ca9-b9a9-c359557a2c4e_7XHPQ-BQMYG-YBP49-CY8B2-T8%f%CGQ_ByPass________AccessRuntimeR |
||||||
|
14_95ab3ec8-4106-4f9d-b632-03c019d1d23f_89RTQ-MT4GK-6CPTX-WWP7C-J9%f%KXR_MAK___________AccessVL |
||||||
|
14_71dc86ff-f056-40d0-8ffb-9592705c9b76_39TRR-C2F37-9WYJ2-MJQXH-B9%f%38K_MAK___________ExcelVL |
||||||
|
14_fdad0dfa-417d-4b4f-93e4-64ea8867b7fd_RCGT3-FPQDV-H49CD-PPDBF-TH%f%47G_MAK___________GrooveVL |
||||||
|
14_7b7d1f17-fdcb-4820-9789-9bec6e377821_3YR9B-D9W79-BY66R-R8XYP-QY%f%YYY_Retail________HomeBusinessR_[HomeBusinessDemoR] |
||||||
|
14_09e2d37e-474b-4121-8626-58ad9be5776f_3X43R-HHHXX-FRHRW-2M2WJ-8V%f%PHD_Retail________HomeStudentR_[HomeStudentDemoR] |
||||||
|
14_ef1da464-01c8-43a6-91af-e4e5713744f9_XDGJY-KFHW9-JWX9X-YM4GW-GC%f%8WR_Retail________InfoPathR |
||||||
|
14_85e22450-b741-430c-a172-a37962c938af_6GKT2-KMJPK-4RRBF-8VQKB-JB%f%6G6_MAK___________InfoPathVL |
||||||
|
14_3f7aa693-9a7e-44fc-9309-bb3d8e604925_2TG3P-9DB76-4YT99-8RXGD-CW%f%XBP_Retail________OneNoteR_[HSOneNoteR] |
||||||
|
14_6860b31f-6a67-48b8-84b9-e312b3485c4b_CV64P-F4VRH-BJ33D-PH6MR-X6%f%9RY_MAK___________OneNoteVL |
||||||
|
14_fbf4ac36-31c8-4340-8666-79873129cf40_9D8FR-7GYBW-4YG8M-V36JK-VD%f%7CM_Retail________OutlookR |
||||||
|
14_a9aeabd8-63b8-4079-a28e-f531807fd6b8_J8C9M-YXMH2-9CX44-2C3YG-V7%f%692_MAK___________OutlookVL |
||||||
|
14_acb51361-c0db-4895-9497-1831c41f31a6_GMBWM-WVX26-7WHV4-DB43D-WV%f%DY2_Retail________PersonalR_[PersonalDemoR,PersonalPrepaidR] |
||||||
|
14_38252940-718c-4aa6-81a4-135398e53851_HPBQP-RJHDR-Q3472-PT9Q6-PB%f%B72_MAK___________PowerPointVL |
||||||
|
14_8b559c37-0117-413e-921b-b853aeb6e210_367X9-9HP9R-TKHY6-DH4QH-K9%f%PY7_Retail________ProfessionalR_[ProfessionalAcadR,ProfessionalDemoR,OEM-SingleImage] |
||||||
|
14_725714d7-d58f-4d12-9fa8-35873c6f7215_6JD4G-KRW3J-48MGV-DM6FC-T9%f%WKR_Retail________ProjectProR_[ProjectProMSDNR] |
||||||
|
14_1cf57a59-c532-4e56-9a7d-ffa2fe94b474_3XDTH-MMGJ6-F9MKX-THP8D-G9%f%BP7_MAK___________ProjectProVL |
||||||
|
14_688f6589-2bd9-424e-a152-b13f36aa6de1_2W96V-RTQ9R-2BPVT-PT8H9-MV%f%68T_Retail________ProjectStdR |
||||||
|
14_11b39439-6b93-4642-9570-f2eb81be2238_4DTT4-D4MKX-23KFH-JKR6T-YK%f%G2J_MAK___________ProjectStdVL |
||||||
|
14_71af7e84-93e6-4363-9b69-699e04e74071_2J9H6-H4D3G-PCXD2-96XVM-TR%f%R73_Retail________ProPlusR_[ProPlusAcadR,ProPlusMSDNR,Sub4R] |
||||||
|
14_fdf3ecb9-b56f-43b2-a9b8-1b48b6bae1a7_6CD6C-9R8PB-T2D9Y-8RKKX-W7%f%DFK_MAK___________ProPlusVL_[ProPlusAcadVL] |
||||||
|
14_98677603-a668-4fa4-9980-3f1f05f78f69_CTRJP-P72VV-JBF8Y-4W6WW-HX%f%HVG_Retail________PublisherR |
||||||
|
14_3d014759-b128-4466-9018-e80f6320d9d0_32YG9-3VX77-YXJVV-PRVFW-TT%f%8BV_MAK___________PublisherVL |
||||||
|
14_8090771e-d41a-4482-929e-de87f1f47e46_7VKXH-9BWCG-RPTBB-JBRV3-GR%f%HYC_MAK___________SmallBusBasicsVL |
||||||
|
14_b78df69e-0966-40b1-ae85-30a5134dedd0_H48K6-FB4Y6-P83GH-9J7XG-HD%f%KKX_ByPass________SPDR |
||||||
|
14_b6d2565c-341d-4768-ad7d-addbe00bb5ce_W3BTX-H6BW7-Q6DFW-BXFFY-8R%f%VJP_Retail________StandardR_[StandardMSDNR][KeyisforMSDNR] |
||||||
|
14_1f76e346-e0be-49bc-9954-70ec53a4fcfe_2XTQP-GDR7C-GTXPC-6W6PV-4R%f%XGC_MAK___________StandardVL_[StandardAcadVL] |
||||||
|
14_2745e581-565a-4670-ae90-6bf7c57ffe43_VXHHB-W7HBD-7M342-RJ7P8-CH%f%BD6_ByPass________StarterR |
||||||
|
14_66cad568-c2dc-459d-93ec-2f3cb967ee34_2RDPT-WPYQM-C2WXF-BTPDW-2J%f%2HM_Retail________VisioSIR_Prem[Pro,Std] |
||||||
|
14_36756cb8-8e69-4d11-9522-68899507cd6a_7PKFT-X2MKQ-GT6X2-8CB2W-CH%f%C9K_MAK___________VisioSIVL_Prem[Pro,Std] |
||||||
|
14_98d4050e-9c98-49bf-9be1-85e12eb3ab13_6J3XK-DFKGK-X373V-QJHYM-V3%f%FC2_MAK___________WordVL |
||||||
|
:: Office 2013 |
||||||
|
15_ab4d047b-97cf-4126-a69f-34df08e2f254_B7RFY-7NXPK-Q4342-Y9X2H-3J%f%X4X_Retail________AccessRetail |
||||||
|
15_259de5be-492b-44b3-9d78-9645f848f7b0_X3XNB-HJB7K-66THH-8DWQ3-XH%f%GJP_Bypass________AccessRuntimeRetail |
||||||
|
15_4374022d-56b8-48c1-9bb7-d8f2fc726343_9MF9G-CN32B-HV7XT-9XJ8T-9K%f%VF4_MAK___________AccessVolume |
||||||
|
15_1b1d9bd5-12ea-4063-964c-16e7e87d6e08_NT889-MBH4X-8MD4H-X8R2D-WQ%f%HF8_Retail________ExcelRetail |
||||||
|
15_ac1ae7fd-b949-4e04-a330-849bc40638cf_Y3N36-YCHDK-XYWBG-KYQVV-BD%f%TJ2_MAK___________ExcelVolume |
||||||
|
15_cfaf5356-49e3-48a8-ab3c-e729ab791250_BMK4W-6N88B-BP9QR-PHFCK-MG%f%7GF_Retail________GrooveRetail |
||||||
|
15_4825ac28-ce41-45a7-9e6e-1fed74057601_RN84D-7HCWY-FTCBK-JMXWM-HT%f%7GJ_MAK___________GrooveVolume |
||||||
|
15_c02fb62e-1cd5-4e18-ba25-e0480467ffaa_2WQNF-GBK4B-XVG6F-BBMX7-M4%f%F2Y_OEM-Perp______HomeBusinessPipcRetail |
||||||
|
15_a2b90e7a-a797-4713-af90-f0becf52a1dd_YWD4R-CNKVT-VG8VJ-9333B-RC%f%W9F_Subscription__HomeBusinessRetail |
||||||
|
15_1fdfb4e4-f9c9-41c4-b055-c80daf00697d_B92QY-NKYFQ-6KTKH-VWW2Q-3P%f%B3B_OEM-ARM_______HomeStudentARMRetail |
||||||
|
15_ebef9f05-5273-404a-9253-c5e252f50555_QPG96-CNT7M-KH36K-KY4HQ-M7%f%TBR_OEM-ARM_______HomeStudentPlusARMRetail |
||||||
|
15_f2de350d-3028-410a-bfae-283e00b44d0e_6WW3N-BDGM9-PCCHD-9QPP9-P3%f%4QG_Subscription__HomeStudentRetail |
||||||
|
15_44984381-406e-4a35-b1c3-e54f499556e2_RV7NQ-HY3WW-7CKWH-QTVMW-29%f%VHC_Retail________InfoPathRetail |
||||||
|
15_9e016989-4007-42a6-8051-64eb97110cf2_C4TGN-QQW6Y-FYKXC-6WJW7-X7%f%3VG_MAK___________InfoPathVolume |
||||||
|
15_9103f3ce-1084-447a-827e-d6097f68c895_6MDN4-WF3FV-4WH3Q-W699V-RG%f%CMY_PrepidBypass__LyncAcademicRetail |
||||||
|
15_ff693bf4-0276-4ddb-bb42-74ef1a0c9f4d_N42BF-CBY9F-W2C7R-X397X-DY%f%FQW_PrepidBypass__LyncEntryRetail |
||||||
|
15_fada6658-bfc6-4c4e-825a-59a89822cda8_89P23-2NK2R-JXM2M-3Q8R8-BW%f%M3Y_Retail________LyncRetail |
||||||
|
15_e1264e10-afaf-4439-a98b-256df8bb156f_3WKCD-RN489-4M7XJ-GJ2GQ-YB%f%FQ6_MAK___________LyncVolume |
||||||
|
15_69ec9152-153b-471a-bf35-77ec88683eae_VNWHF-FKFBW-Q2RGD-HYHWF-R3%f%HH2_Subscription__MondoRetail |
||||||
|
15_f33485a0-310b-4b72-9a0e-b1d605510dbd_2YNYQ-FQMVG-CB8KW-6XKYD-M7%f%RRJ_MAK___________MondoVolume |
||||||
|
15_3391e125-f6e4-4b1e-899c-a25e6092d40d_4TGWV-6N9P6-G2H8Y-2HWKB-B4%f%FF4_Bypass________OneNoteFreeRetail |
||||||
|
15_8b524bcc-67ea-4876-a509-45e46f6347e8_3KXXQ-PVN2C-8P7YY-HCV88-GV%f%GQ6_Retail________OneNoteRetail |
||||||
|
15_b067e965-7521-455b-b9f7-c740204578a2_JDMWF-NJC7B-HRCHY-WFT8G-BP%f%XD9_MAK___________OneNoteVolume |
||||||
|
15_12004b48-e6c8-4ffa-ad5a-ac8d4467765a_9N4RQ-CF8R2-HBVCB-J3C9V-94%f%P4D_Retail________OutlookRetail |
||||||
|
15_8d577c50-ae5e-47fd-a240-24986f73d503_HNG29-GGWRG-RFC8C-JTFP4-2J%f%9FH_MAK___________OutlookVolume |
||||||
|
15_5aab8561-1686-43f7-9ff5-2c861da58d17_9CYB3-NFMRW-YFDG6-XC7TF-BY%f%36J_OEM-Perp______PersonalPipcRetail |
||||||
|
15_17e9df2d-ed91-4382-904b-4fed6a12caf0_2NCQJ-MFRMH-TXV83-J7V4C-RV%f%RWC_Retail________PersonalRetail |
||||||
|
15_31743b82-bfbc-44b6-aa12-85d42e644d5b_HVMN2-KPHQH-DVQMK-7B3CM-FG%f%BFC_Retail________PowerPointRetail |
||||||
|
15_e40dcb44-1d5c-4085-8e8f-943f33c4f004_47DKN-HPJP7-RF9M3-VCYT2-TM%f%Q4G_MAK___________PowerPointVolume |
||||||
|
15_064383fa-1538-491c-859b-0ecab169a0ab_N3QMM-GKDT3-JQGX6-7X3MQ-4G%f%BG3_Retail________ProPlusRetail |
||||||
|
15_2b88c4f2-ea8f-43cd-805e-4d41346e18a7_QKHNX-M9GGH-T3QMW-YPK4Q-QR%f%P9V_MAK___________ProPlusVolume |
||||||
|
15_4e26cac1-e15a-4467-9069-cb47b67fe191_CF9DD-6CNW2-BJWJQ-CVCFX-Y7%f%TXD_OEM-Perp______ProfessionalPipcRetail |
||||||
|
15_44bc70e2-fb83-4b09-9082-e5557e0c2ede_MBQBN-CQPT6-PXRMC-TYJFR-3C%f%8MY_Retail________ProfessionalRetail |
||||||
|
15_2f72340c-b555-418d-8b46-355944fe66b8_WPY8N-PDPY4-FC7TF-KMP7P-KW%f%YFY_Subscription__ProjectProRetail |
||||||
|
15_ed34dc89-1c27-4ecd-8b2f-63d0f4cedc32_WFCT2-NBFQ7-JD7VV-MFJX6-6F%f%2CM_MAK___________ProjectProVolume |
||||||
|
15_58d95b09-6af6-453d-a976-8ef0ae0316b1_NTHQT-VKK6W-BRB87-HV346-Y9%f%6W8_Subscription__ProjectStdRetail |
||||||
|
15_2b9e4a37-6230-4b42-bee2-e25ce86c8c7a_3CNQX-T34TY-99RH4-C4YD2-KW%f%YGV_MAK___________ProjectStdVolume |
||||||
|
15_c3a0814a-70a4-471f-af37-2313a6331111_TWNCJ-YR84W-X7PPF-6DPRP-D6%f%7VC_Retail________PublisherRetail |
||||||
|
15_38ea49f6-ad1d-43f1-9888-99a35d7c9409_DJPHV-NCJV6-GWPT6-K26JX-C7%f%GX6_MAK___________PublisherVolume |
||||||
|
15_ba3e3833-6a7e-445a-89d0-7802a9a68588_3NY6J-WHT3F-47BDV-JHF36-23%f%43W_PrepidBypass__SPDRetail |
||||||
|
15_32255c0a-16b4-4ce2-b388-8a4267e219eb_V6VWN-KC2HR-YYDD6-9V7HQ-7T%f%7VP_Retail________StandardRetail |
||||||
|
15_a24cca51-3d54-4c41-8a76-4031f5338cb2_9TN6B-PCYH4-MCVDQ-KT83C-TM%f%Q7T_MAK___________StandardVolume |
||||||
|
15_a56a3b37-3a35-4bbb-a036-eee5f1898eee_NVK2G-2MY4G-7JX2P-7D6F2-VF%f%QBR_Subscription__VisioProRetail |
||||||
|
15_3e4294dd-a765-49bc-8dbd-cf8b62a4bd3d_YN7CF-XRH6R-CGKRY-GKPV3-BG%f%7WF_MAK___________VisioProVolume |
||||||
|
15_980f9e3e-f5a8-41c8-8596-61404addf677_NCRB7-VP48F-43FYY-62P3R-36%f%7WK_Subscription__VisioStdRetail |
||||||
|
15_44a1f6ff-0876-4edb-9169-dbb43101ee89_RX63Y-4NFK2-XTYC8-C6B3W-YP%f%XPJ_MAK___________VisioStdVolume |
||||||
|
15_191509f2-6977-456f-ab30-cf0492b1e93a_NB77V-RPFQ6-PMMKQ-T87DV-M4%f%D84_Retail________WordRetail |
||||||
|
15_9cedef15-be37-4ff0-a08a-13a045540641_RPHPB-Y7NC4-3VYFM-DW7VD-G8%f%YJ8_MAK___________WordVolume |
||||||
|
:: Office 365 - 15.0 version |
||||||
|
15_742178ed-6b28-42dd-b3d7-b7c0ea78741b_Y9NF9-M2QWD-FF6RJ-QJW36-RR%f%F2T_SubTest_______O365BusinessRetail |
||||||
|
15_a96f8dae-da54-4fad-bdc6-108da592707a_3NMDC-G7C3W-68RGP-CB4MH-4C%f%XCH_SubTest1______O365HomePremRetail |
||||||
|
15_e3dacc06-3bc2-4e13-8e59-8e05f3232325_H8DN8-Y2YP3-CR9JT-DHDR9-C7%f%GP3_Subscription2_O365ProPlusRetail |
||||||
|
15_0bc1dae4-6158-4a1c-a893-807665b934b2_2QCNB-RMDKJ-GC8PB-7QGQV-7Q%f%TQJ_Subscription2_O365SmallBusPremRetail |
||||||
|
:: Office 365 - 16.0 version |
||||||
|
16_dabaa1f2-109b-496d-bf49-1536cc862900_3HYJN-9KG99-F8VG9-V3DT8-JF%f%MHV_Subscription__O365AppsBasicRetail |
||||||
|
16_742178ed-6b28-42dd-b3d7-b7c0ea78741b_Y9NF9-M2QWD-FF6RJ-QJW36-RR%f%F2T_SubTest_______O365BusinessRetail |
||||||
|
16_2f5c71b4-5b7a-4005-bb68-f9fac26f2ea3_W62NQ-267QR-RTF74-PF2MH-JQ%f%MTH_Subscription__O365EduCloudRetail |
||||||
|
16_a96f8dae-da54-4fad-bdc6-108da592707a_3NMDC-G7C3W-68RGP-CB4MH-4C%f%XCH_SubTest1______O365HomePremRetail |
||||||
|
16_e3dacc06-3bc2-4e13-8e59-8e05f3232325_H8DN8-Y2YP3-CR9JT-DHDR9-C7%f%GP3_Subscription2_O365ProPlusRetail |
||||||
|
16_0bc1dae4-6158-4a1c-a893-807665b934b2_2QCNB-RMDKJ-GC8PB-7QGQV-7Q%f%TQJ_Subscription2_O365SmallBusPremRetail |
||||||
|
:: Office 2016 |
||||||
|
16_bfa358b0-98f1-4125-842e-585fa13032e6_WHK4N-YQGHB-XWXCC-G3HYC-6J%f%F94_Retail________AccessRetail |
||||||
|
16_9d9faf9e-d345-4b49-afce-68cb0a539c7c_RNB7V-P48F4-3FYY6-2P3R3-63%f%BQV_PrepidBypass__AccessRuntimeRetail |
||||||
|
16_3b2fa33f-cd5a-43a5-bd95-f49f3f546b0b_JJ2Y4-N8KM3-Y8KY3-Y22FR-R3%f%KVK_MAK___________AccessVolume |
||||||
|
16_424d52ff-7ad2-4bc7-8ac6-748d767b455d_RKJBN-VWTM2-BDKXX-RKQFD-JT%f%YQ2_Retail________ExcelRetail |
||||||
|
16_685062a7-6024-42e7-8c5f-6bb9e63e697f_FVGNR-X82B2-6PRJM-YT4W7-8H%f%V36_MAK___________ExcelVolume |
||||||
|
16_c02fb62e-1cd5-4e18-ba25-e0480467ffaa_2WQNF-GBK4B-XVG6F-BBMX7-M4%f%F2Y_OEM-Perp______HomeBusinessPipcRetail |
||||||
|
16_86834d00-7896-4a38-8fae-32f20b86fa2b_HM6FM-NVF78-KV9PM-F36B8-D9%f%MXD_Retail________HomeBusinessRetail |
||||||
|
16_090896a0-ea98-48ac-b545-ba5da0eb0c9c_PBQPJ-NC22K-69MXD-KWMRF-WF%f%G77_OEM-ARM_______HomeStudentARMRetail |
||||||
|
16_6bbe2077-01a4-4269-bf15-5bf4d8efc0b2_6F2NY-7RTX4-MD9KM-TJ43H-94%f%TBT_OEM-ARM_______HomeStudentPlusARMRetail |
||||||
|
16_c28acdb8-d8b3-4199-baa4-024d09e97c99_PNPRV-F2627-Q8JVC-3DGR9-WT%f%YRK_Retail________HomeStudentRetail |
||||||
|
16_e2127526-b60c-43e0-bed1-3c9dc3d5a468_YWD4R-CNKVT-VG8VJ-9333B-RC%f%3B8_Retail________HomeStudentVNextRetail |
||||||
|
16_69ec9152-153b-471a-bf35-77ec88683eae_VNWHF-FKFBW-Q2RGD-HYHWF-R3%f%HH2_Subscription__MondoRetail |
||||||
|
16_2cd0ea7e-749f-4288-a05e-567c573b2a6c_FMTQQ-84NR8-2744R-MXF4P-PG%f%YR3_MAK___________MondoVolume |
||||||
|
16_436366de-5579-4f24-96db-3893e4400030_XYNTG-R96FY-369HX-YFPHY-F9%f%CPM_Bypass________OneNoteFreeRetail |
||||||
|
16_83ac4dd9-1b93-40ed-aa55-ede25bb6af38_FXF6F-CNC26-W643C-K6KB7-6X%f%XW3_Retail________OneNoteRetail |
||||||
|
16_23b672da-a456-4860-a8f3-e062a501d7e8_9TYVN-D76HK-BVMWT-Y7G88-9T%f%PPV_MAK___________OneNoteVolume |
||||||
|
16_5a670809-0983-4c2d-8aad-d3c2c5b7d5d1_7N4KG-P2QDH-86V9C-DJFVF-36%f%9W9_Retail________OutlookRetail |
||||||
|
16_50059979-ac6f-4458-9e79-710bcb41721a_7QPNR-3HFDG-YP6T9-JQCKQ-KK%f%XXC_MAK___________OutlookVolume |
||||||
|
16_5aab8561-1686-43f7-9ff5-2c861da58d17_9CYB3-NFMRW-YFDG6-XC7TF-BY%f%36J_OEM-Perp______PersonalPipcRetail |
||||||
|
16_a9f645a1-0d6a-4978-926a-abcb363b72a6_FT7VF-XBN92-HPDJV-RHMBY-6V%f%KBF_Retail________PersonalRetail |
||||||
|
16_f32d1284-0792-49da-9ac6-deb2bc9c80b6_N7GCB-WQT7K-QRHWG-TTPYD-7T%f%9XF_Retail________PowerPointRetail |
||||||
|
16_9b4060c9-a7f5-4a66-b732-faf248b7240f_X3RT9-NDG64-VMK2M-KQ6XY-DP%f%FGV_MAK___________PowerPointVolume |
||||||
|
16_de52bd50-9564-4adc-8fcb-a345c17f84f9_GM43N-F742Q-6JDDK-M622J-J8%f%GDV_Retail________ProPlusRetail |
||||||
|
16_c47456e3-265d-47b6-8ca0-c30abbd0ca36_FNVK8-8DVCJ-F7X3J-KGVQB-RC%f%2QY_MAK___________ProPlusVolume |
||||||
|
16_4e26cac1-e15a-4467-9069-cb47b67fe191_CF9DD-6CNW2-BJWJQ-CVCFX-Y7%f%TXD_OEM-Perp______ProfessionalPipcRetail |
||||||
|
16_d64edc00-7453-4301-8428-197343fafb16_NXFTK-YD9Y7-X9MMJ-9BWM6-J2%f%QVH_Retail________ProfessionalRetail |
||||||
|
16_2f72340c-b555-418d-8b46-355944fe66b8_WPY8N-PDPY4-FC7TF-KMP7P-KW%f%YFY_Subscription__ProjectProRetail |
||||||
|
16_82f502b5-b0b0-4349-bd2c-c560df85b248_PKC3N-8F99H-28MVY-J4RYY-CW%f%GDH_MAK___________ProjectProVolume |
||||||
|
16_16728639-a9ab-4994-b6d8-f81051e69833_JBNPH-YF2F7-Q9Y29-86CTG-C9%f%YGV_MAKC2R________ProjectProXVolume |
||||||
|
16_58d95b09-6af6-453d-a976-8ef0ae0316b1_NTHQT-VKK6W-BRB87-HV346-Y9%f%6W8_Subscription__ProjectStdRetail |
||||||
|
16_82e6b314-2a62-4e51-9220-61358dd230e6_4TGWV-6N9P6-G2H8Y-2HWKB-B4%f%G93_MAK___________ProjectStdVolume |
||||||
|
16_431058f0-c059-44c5-b9e7-ed2dd46b6789_N3W2Q-69MBT-27RD9-BH8V3-JT%f%2C8_MAKC2R________ProjectStdXVolume |
||||||
|
16_6e0c1d99-c72e-4968-bcb7-ab79e03e201e_WKWND-X6G9G-CDMTV-CPGYJ-6M%f%VBF_Retail________PublisherRetail |
||||||
|
16_fcc1757b-5d5f-486a-87cf-c4d6dedb6032_9QVN2-PXXRX-8V4W8-Q7926-TJ%f%GD8_MAK___________PublisherVolume |
||||||
|
16_9103f3ce-1084-447a-827e-d6097f68c895_6MDN4-WF3FV-4WH3Q-W699V-RG%f%CMY_PrepidBypass__SkypeServiceBypassRetail |
||||||
|
16_971cd368-f2e1-49c1-aedd-330909ce18b6_4N4D8-3J7Y3-YYW7C-73HD2-V8%f%RHY_PrepidBypass__SkypeforBusinessEntryRetail |
||||||
|
16_418d2b9f-b491-4d7f-84f1-49e27cc66597_PBJ79-77NY4-VRGFG-Y8WYC-CK%f%CRC_Retail________SkypeforBusinessRetail |
||||||
|
16_03ca3b9a-0869-4749-8988-3cbc9d9f51bb_DMTCJ-KNRKR-JV8TQ-V2CR2-VF%f%TFH_MAK___________SkypeforBusinessVolume |
||||||
|
16_4a31c291-3a12-4c64-b8ab-cd79212be45e_2FPWN-4H6CM-KD8QQ-8HCHC-P9%f%XYW_Retail________StandardRetail |
||||||
|
16_0ed94aac-2234-4309-ba29-74bdbb887083_WHGMQ-JNMGT-MDQVF-WDR69-KQ%f%BWC_MAK___________StandardVolume |
||||||
|
16_a56a3b37-3a35-4bbb-a036-eee5f1898eee_NVK2G-2MY4G-7JX2P-7D6F2-VF%f%QBR_Subscription__VisioProRetail |
||||||
|
16_295b2c03-4b1c-4221-b292-1411f468bd02_NRKT9-C8GP2-XDYXQ-YW72K-MG%f%92B_MAK___________VisioProVolume |
||||||
|
16_0594dc12-8444-4912-936a-747ca742dbdb_G98Q2-B6N77-CFH9J-K824G-XQ%f%CC4_MAKC2R________VisioProXVolume |
||||||
|
16_980f9e3e-f5a8-41c8-8596-61404addf677_NCRB7-VP48F-43FYY-62P3R-36%f%7WK_Subscription__VisioStdRetail |
||||||
|
16_44151c2d-c398-471f-946f-7660542e3369_XNCJB-YY883-JRW64-DPXMX-JX%f%CR6_MAK___________VisioStdVolume |
||||||
|
16_1d1c6879-39a3-47a5-9a6d-aceefa6a289d_B2HTN-JPH8C-J6Y6V-HCHKB-43%f%MGT_MAKC2R________VisioStdXVolume |
||||||
|
16_cacaa1bf-da53-4c3b-9700-11738ef1c2a5_P8K82-NQ7GG-JKY8T-6VHVY-88%f%GGD_Retail________WordRetail |
||||||
|
16_c3000759-551f-4f4a-bcac-a4b42cbf1de2_YHMWC-YN6V9-WJPXD-3WQKP-TM%f%VCV_MAK___________WordVolume |
||||||
|
:: Office 2019 |
||||||
|
16_518687bd-dc55-45b9-8fa6-f918e1082e83_WRYJ6-G3NP7-7VH94-8X7KP-JB%f%7HC_Retail________Access2019Retail |
||||||
|
16_385b91d6-9c2c-4a2e-86b5-f44d44a48c5f_6FWHX-NKYXK-BW34Q-7XC9F-Q9%f%PX7_MAK-AE________Access2019Volume |
||||||
|
16_22e6b96c-1011-4cd5-8b35-3c8fb6366b86_FGQNJ-JWJCG-7Q8MG-RMRGJ-9T%f%QVF_PrepidBypass__AccessRuntime2019Retail |
||||||
|
16_c201c2b7-02a1-41a8-b496-37c72910cd4a_KBPNW-64CMM-8KWCB-23F44-8B%f%7HM_Retail________Excel2019Retail |
||||||
|
16_05cb4e1d-cc81-45d5-a769-f34b09b9b391_8NT4X-GQMCK-62X4P-TW6QP-YK%f%PYF_MAK-AE________Excel2019Volume |
||||||
|
16_7fe09eef-5eed-4733-9a60-d7019df11cac_QBN2Y-9B284-9KW78-K48PB-R6%f%2YT_Retail________HomeBusiness2019Retail |
||||||
|
16_6303d14a-afad-431f-8434-81052a65f575_DJTNY-4HDWM-TDWB2-8PWC2-W2%f%RRT_OEM-ARM_______HomeStudentARM2019Retail |
||||||
|
16_215c841d-ffc1-4f03-bd11-5b27b6ab64cc_NM8WT-CFHB2-QBGXK-J8W6J-GV%f%K8F_OEM-ARM_______HomeStudentPlusARM2019Retail |
||||||
|
16_4539aa2c-5c31-4d47-9139-543a868e5741_XNWPM-32XQC-Y7QJC-QGGBV-YY%f%7JK_Retail________HomeStudent2019Retail |
||||||
|
16_20e359d5-927f-47c0-8a27-38adbdd27124_WR43D-NMWQQ-HCQR2-VKXDR-37%f%B7H_Retail________Outlook2019Retail |
||||||
|
16_92a99ed8-2923-4cb7-a4c5-31da6b0b8cf3_RN3QB-GT6D7-YB3VH-F3RPB-3G%f%QYB_MAK-AE________Outlook2019Volume |
||||||
|
16_2747b731-0f1f-413e-a92d-386ec1277dd8_NMBY8-V3CV7-BX6K6-2922Y-43%f%M7T_Retail________Personal2019Retail |
||||||
|
16_7e63cc20-ba37-42a1-822d-d5f29f33a108_HN27K-JHJ8R-7T7KK-WJYC3-FM%f%7MM_Retail________PowerPoint2019Retail |
||||||
|
16_13c2d7bf-f10d-42eb-9e93-abf846785434_29GNM-VM33V-WR23K-HG2DT-KT%f%QYR_MAK-AE________PowerPoint2019Volume |
||||||
|
16_a3072b8f-adcc-4e75-8d62-fdeb9bdfae57_BN4XJ-R9DYY-96W48-YK8DM-MY%f%7PY_Retail________ProPlus2019Retail |
||||||
|
16_6755c7a7-4dfe-46f5-bce8-427be8e9dc62_T8YBN-4YV3X-KK24Q-QXBD7-T3%f%C63_MAK-AE________ProPlus2019Volume |
||||||
|
16_1717c1e0-47d3-4899-a6d3-1022db7415e0_9NXDK-MRY98-2VJV8-GF73J-TQ%f%9FK_Retail________Professional2019Retail |
||||||
|
16_0d270ef7-5aaf-4370-a372-bc806b96adb7_JDTNC-PP77T-T9H2W-G4J2J-VH%f%8JK_Retail________ProjectPro2019Retail |
||||||
|
16_d4ebadd6-401b-40d5-adf4-a5d4accd72d1_TBXBD-FNWKJ-WRHBD-KBPHH-XD%f%9F2_MAK-AE________ProjectPro2019Volume |
||||||
|
16_bb7ffe5f-daf9-4b79-b107-453e1c8427b5_R3JNT-8PBDP-MTWCK-VD2V8-HM%f%KF9_Retail________ProjectStd2019Retail |
||||||
|
16_fdaa3c03-dc27-4a8d-8cbf-c3d843a28ddc_RBRFX-MQNDJ-4XFHF-7QVDR-JH%f%XGC_MAK-AE________ProjectStd2019Volume |
||||||
|
16_f053a7c7-f342-4ab8-9526-a1d6e5105823_4QC36-NW3YH-D2Y9D-RJPC7-VV%f%B9D_Retail________Publisher2019Retail |
||||||
|
16_40055495-be00-444e-99cc-07446729b53e_K8F2D-NBM32-BF26V-YCKFJ-29%f%Y9W_MAK-AE________Publisher2019Volume |
||||||
|
16_b639e55c-8f3e-47fe-9761-26c6a786ad6b_JBDKF-6NCD6-49K3G-2TV79-BK%f%P73_Retail________SkypeforBusiness2019Retail |
||||||
|
16_15a430d4-5e3f-4e6d-8a0a-14bf3caee4c7_9MNQ7-YPQ3B-6WJXM-G83T3-CB%f%BDK_MAK-AE________SkypeforBusiness2019Volume |
||||||
|
16_f88cfdec-94ce-4463-a969-037be92bc0e7_N9722-BV9H6-WTJTT-FPB93-97%f%8MK_PrepidBypass__SkypeforBusinessEntry2019Retail |
||||||
|
16_fdfa34dd-a472-4b85-bee6-cf07bf0aaa1c_NDGVM-MD27H-2XHVC-KDDX2-YK%f%P74_Retail________Standard2019Retail |
||||||
|
16_beb5065c-1872-409e-94e2-403bcfb6a878_NT3V6-XMBK7-Q66MF-VMKR4-FC%f%33M_MAK-AE________Standard2019Volume |
||||||
|
16_a6f69d68-5590-4e02-80b9-e7233dff204e_2NWVW-QGF4T-9CPMB-WYDQ9-7X%f%P79_Retail________VisioPro2019Retail |
||||||
|
16_f41abf81-f409-4b0d-889d-92b3e3d7d005_33YF4-GNCQ3-J6GDM-J67P3-FM%f%7QP_MAK-AE________VisioPro2019Volume |
||||||
|
16_4a582021-18c2-489f-9b3d-5186de48f1cd_263WK-3N797-7R437-28BKG-3V%f%8M8_Retail________VisioStd2019Retail |
||||||
|
16_933ed0e3-747d-48b0-9c2c-7ceb4c7e473d_BGNHX-QTPRJ-F9C9G-R8QQG-8T%f%27F_MAK-AE________VisioStd2019Volume |
||||||
|
16_72cee1c2-3376-4377-9f25-4024b6baadf8_JXR8H-NJ3MK-X66W8-78CWD-QR%f%VR2_Retail________Word2019Retail |
||||||
|
16_fe5fe9d5-3b06-4015-aa35-b146f85c4709_9F36R-PNVHH-3DXGQ-7CD2H-R9%f%D3V_MAK-AE________Word2019Volume |
||||||
|
:: Office 2021 |
||||||
|
16_f634398e-af69-48c9-b256-477bea3078b5_P286B-N3XYP-36QRQ-29CMP-RV%f%X9M_Retail________Access2021Retail |
||||||
|
16_ae17db74-16b0-430b-912f-4fe456e271db_JBH3N-P97FP-FRTJD-MGK2C-VF%f%WG6_MAK-AE________Access2021Volume |
||||||
|
16_844c36cb-851c-49e7-9079-12e62a049e2a_MNX9D-PB834-VCGY2-K2RW2-2D%f%P3D_Bypass________AccessRuntime2021Retail |
||||||
|
16_fb099c19-d48b-4a2f-a160-4383011060aa_V6QFB-7N7G9-PF7W9-M8FQM-MY%f%8G9_Retail________Excel2021Retail |
||||||
|
16_9da1ecdb-3a62-4273-a234-bf6d43dc0778_WNYR4-KMR9H-KVC8W-7HJ8B-K7%f%9DQ_MAK-AE________Excel2021Volume |
||||||
|
16_38b92b63-1dff-4be7-8483-2a839441a2bc_JM99N-4MMD8-DQCGJ-VMYFY-R6%f%3YK_Subscription__HomeBusiness2021Retail |
||||||
|
16_2f258377-738f-48dd-9397-287e43079958_N3CWD-38XVH-KRX2Y-YRP74-6R%f%BB2_Subscription__HomeStudent2021Retail |
||||||
|
16_279706f4-3a4b-4877-949b-f8c299cf0cc5_NB2TQ-3Y79C-77C6M-QMY7H-7Q%f%Y8P_Retail________OneNote2021Retail |
||||||
|
16_0c7af60d-0664-49fc-9b01-41b2dea81380_THNKC-KFR6C-Y86Q9-W8CB3-GF%f%7PD_MAK-AE________OneNote2021Volume |
||||||
|
16_778ccb9a-2f6a-44e5-853c-eb22b7609643_CNM3W-V94GB-QJQHH-BDQ3J-33%f%Y8H_Bypass________OneNoteFree2021Retail |
||||||
|
16_ecea2cfa-d406-4a7f-be0d-c6163250d126_4NCWR-9V92Y-34VB2-RPTHR-YT%f%GR7_Retail________Outlook2021Retail |
||||||
|
16_45bf67f9-0fc8-4335-8b09-9226cef8a576_JQ9MJ-QYN6B-67PX9-GYFVY-QJ%f%6TB_MAK-AE________Outlook2021Volume |
||||||
|
16_8f89391e-eedb-429d-af90-9d36fbf94de6_RRRYB-DN749-GCPW4-9H6VK-HC%f%HPT_Retail________Personal2021Retail |
||||||
|
16_c9bf5e86-f5e3-4ac6-8d52-e114a604d7bf_3KXXQ-PVN2C-8P7YY-HCV88-GV%f%M96_Retail1_______PowerPoint2021Retail |
||||||
|
16_716f2434-41b6-4969-ab73-e61e593a3875_39G2N-3BD9C-C4XCM-BD4QG-FV%f%YDY_MAK-AE________PowerPoint2021Volume |
||||||
|
16_c2f04adf-a5de-45c5-99a5-f5fddbda74a8_8WXTP-MN628-KY44G-VJWCK-C7%f%PCF_Retail________ProPlus2021Retail |
||||||
|
16_3f180b30-9b05-4fe2-aa8d-0c1c4790f811_RNHJY-DTFXW-HW9F8-4982D-MD%f%2CW_MAK-AE1_______ProPlus2021Volume |
||||||
|
16_96097a68-b5c5-4b19-8600-2e8d6841a0db_JRJNJ-33M7C-R73X3-P9XF7-R9%f%F6M_MAK-AE________ProPlusSPLA2021Volume |
||||||
|
16_711e48a6-1a79-4b00-af10-73f4ca3aaac4_DJPHV-NCJV6-GWPT6-K26JX-C7%f%PBG_Retail________Professional2021Retail |
||||||
|
16_3747d1d5-55a8-4bc3-b53d-19fff1913195_QKHNX-M9GGH-T3QMW-YPK4Q-QR%f%WMV_Retail________ProjectPro2021Retail |
||||||
|
16_17739068-86c4-4924-8633-1e529abc7efc_HVC34-CVNPG-RVCMT-X2JRF-CR%f%7RK_MAK-AE1_______ProjectPro2021Volume |
||||||
|
16_4ea64dca-227c-436b-813f-b6624be2d54c_2B96V-X9NJY-WFBRC-Q8MP2-7C%f%HRR_Retail________ProjectStd2021Retail |
||||||
|
16_84313d1e-47c8-4e27-8ced-0476b7ee46c4_3CNQX-T34TY-99RH4-C4YD2-KW%f%6WH_MAK-AE________ProjectStd2021Volume |
||||||
|
16_b769b746-53b1-4d89-8a68-41944dafe797_CDNFG-77T8D-VKQJX-B7KT3-KK%f%28V_Retail1_______Publisher2021Retail |
||||||
|
16_a0234cfe-99bd-4586-a812-4f296323c760_2KXJH-3NHTW-RDBPX-QFRXJ-MT%f%GXF_MAK-AE________Publisher2021Volume |
||||||
|
16_c3fb48b2-1fd4-4dc8-af39-819edf194288_DVBXN-HFT43-CVPRQ-J89TF-VM%f%MHG_Retail________SkypeforBusiness2021Retail |
||||||
|
16_6029109c-ceb8-4ee5-b324-f8eb2981e99a_R3FCY-NHGC7-CBPVP-8Q934-YT%f%GXG_MAK-AE________SkypeforBusiness2021Volume |
||||||
|
16_9e7e7b8e-a0e7-467b-9749-d0de82fb7297_HXNXB-J4JGM-TCF44-2X2CV-FJ%f%VVH_Retail________Standard2021Retail |
||||||
|
16_223a60d8-9002-4a55-abac-593f5b66ca45_2CJN4-C9XK2-HFPQ6-YH498-82%f%TXH_MAK-AE________Standard2021Volume |
||||||
|
16_b99ba8c4-e257-4b70-a31a-8bd308ce7073_BQWDW-NJ9YF-P7Y79-H6DCT-MK%f%Q9C_MAK-AE________StandardSPLA2021Volume |
||||||
|
16_814014d3-c30b-4f63-a493-3708e0dc0ba8_T6P26-NJVBR-76BK8-WBCDY-TX%f%3BC_Retail________VisioPro2021Retail |
||||||
|
16_c590605a-a08a-4cc7-8dc2-f1ffb3d06949_JNKBX-MH9P4-K8YYV-8CG2Y-VQ%f%2C8_MAK-AE________VisioPro2021Volume |
||||||
|
16_16d43989-a5ef-47e2-9ff1-272784caee24_89NYY-KB93R-7X22F-93QDF-DJ%f%6YM_Retail________VisioStd2021Retail |
||||||
|
16_d55f90ee-4ba2-4d02-b216-1300ee50e2af_BW43B-4PNFP-V637F-23TR2-J4%f%7TX_MAK-AE________VisioStd2021Volume |
||||||
|
16_fb33d997-4aa3-494e-8b58-03e9ab0f181d_VNCC4-CJQVK-BKX34-77Y8H-CY%f%XMR_Retail________Word2021Retail |
||||||
|
16_0c728382-95fb-4a55-8f12-62e605f91727_BJG97-NW3GM-8QQQ7-FH76G-68%f%6XM_MAK-AE________Word2021Volume |
||||||
|
:: Office 2024 |
||||||
|
16_8fdb1f1e-663f-4f2e-8fdb-7c35aee7d5ea_GNXWX-DF797-B2JT3-82W27-KH%f%PXT_MAK-AE________ProPlus2024Volume-Preview |
||||||
|
16_33b11b14-91fd-4f7b-b704-e64a055cf601_X86XX-N3QMW-B4WGQ-QCB69-V2%f%6KW_MAK-AE________ProjectPro2024Volume-Preview |
||||||
|
16_eb074198-7384-4bdd-8e6c-c3342dac8435_DW99Y-H7NT6-6B29D-8JQ8F-R3%f%QT7_MAK-AE________VisioPro2024Volume-Preview |
||||||
|
16_e563d108-7b0e-418a-8390-20e1d133d6bb_P6NMW-JMTRC-R6MQ6-HH3F2-BT%f%HKB_Retail________Access2024Retail |
||||||
|
16_f748e2f7-5951-4bc2-8a06-5a1fbe42f5f4_CXNJT-98HPP-92HX7-MX6GY-2P%f%VFR_MAK-AE________Access2024Volume |
||||||
|
16_f3a5e86a-e4f8-4d88-8220-1440c3bbcefa_82CNJ-W82TW-BY23W-BVJ6W-W4%f%8GP_Retail________Excel2024Retail |
||||||
|
16_523fbbab-c290-460d-a6c9-48e49709cb8e_7Y287-9N2KC-8MRR3-BKY82-2D%f%QRV_MAK-AE________Excel2024Volume |
||||||
|
16_885f83e0-5e18-4199-b8be-56697d0debfb_N69X7-73KPT-899FD-P8HQ4-QG%f%TP4_Retail________Home2024Retail |
||||||
|
16_acd4eccb-ff89-4e6a-9350-d2d56276ec69_PRKQM-YNPQR-77QT6-328D7-BD%f%223_Retail________HomeBusiness2024Retail |
||||||
|
16_6f5fd645-7119-44a4-91b4-eccfeeb738bf_2CFK4-N44KG-7XG89-CWDG6-P7%f%P27_Retail________Outlook2024Retail |
||||||
|
16_9a1e1bac-2d8b-4890-832f-0a68b27c16e0_NQPXP-WVB87-H3MMB-FYBW2-9Q%f%FPB_MAK-AE________Outlook2024Volume |
||||||
|
16_da9a57ae-81a8-4cb3-b764-5840e6b5d0bf_CT2KT-GTNWH-9HFGW-J2PWJ-XW%f%7KJ_Retail________PowerPoint2024Retail |
||||||
|
16_eca0d8a6-e21b-4622-9a87-a7103ff14012_RRXFN-JJ26R-RVWD2-V7WMP-27%f%PWQ_MAK-AE________PowerPoint2024Volume |
||||||
|
16_295dcc21-151a-4b4d-8f50-2b627ea197f6_GNJ6P-Y4RBM-C32WW-2VJKJ-MT%f%HKK_Retail________ProjectPro2024Retail |
||||||
|
16_2141d341-41aa-4e45-9ca1-201e117d6495_WNFMR-HK4R7-7FJVM-VQ3JC-76%f%HF6_MAK-AE1_______ProjectPro2024Volume |
||||||
|
16_ead42f74-817d-45b4-af6b-3beeb36ba650_C2PNM-2GQFC-CY3XR-WXCP4-GX%f%3XM_Retail________ProjectStd2024Retail |
||||||
|
16_4b6d9b9b-c16e-429d-babe-8bb84c3c27d6_F2VNW-MW8TT-K622Q-4D96H-PW%f%J8X_MAK-AE________ProjectStd2024Volume |
||||||
|
16_db249714-bb54-4422-8c78-2cc8d4c4a19f_VWCNX-7FKBD-FHJYG-XBR4B-88%f%KC6_Retail________ProPlus2024Retail |
||||||
|
16_d77244dc-2b82-4f0a-b8ae-1fca00b7f3e2_4YV2J-VNG7W-YGTP3-443TK-TF%f%8CP_MAK-AE1_______ProPlus2024Volume |
||||||
|
16_3046a03e-2277-4a51-8ccd-a6609eae8c19_XKRBW-KN2FF-G8CKY-HXVG6-FV%f%Y2V_MAK-AE________SkypeforBusiness2024Volume |
||||||
|
16_44a07f51-8263-4b2f-b2a5-70340055c646_GVG6N-6WCHH-K2MVP-RQ78V-3J%f%7GJ_MAK-AE1_______Standard2024Volume |
||||||
|
16_282d8f34-1111-4a6f-80fe-c17f70dec567_HGRBX-N68QF-6DY8J-CGX4W-XW%f%7KP_Retail________VisioPro2024Retail |
||||||
|
16_4c2f32bf-9d0b-4d8c-8ab1-b4c6a0b9992d_GBNHB-B2G3Q-G42YB-3MFC2-7C%f%JCX_MAK-AE________VisioPro2024Volume |
||||||
|
16_8504167d-887a-41ae-bd1d-f849d834352d_VBXPJ-38NR3-C4DKF-C8RT7-RG%f%HKQ_Retail________VisioStd2024Retail |
||||||
|
16_0978336b-5611-497c-9414-96effaff4938_YNFTY-63K7P-FKHXK-28YYT-D3%f%2XB_MAK-AE________VisioStd2024Volume |
||||||
|
16_f6b24e61-6aa7-4fd2-ab9b-4046cee4230a_XN33R-RP676-GMY2F-T3MH7-GC%f%VKR_Retail________Word2024Retail |
||||||
|
16_06142aa2-e935-49ca-af5d-08069a3d84f3_WD8CQ-6KNQM-8W2CX-2RT63-KK%f%3TP_MAK-AE________Word2024Volume |
||||||
|
"#; |
||||||
|
|
||||||
|
/// `:msiofficedata` — `ver_activationId_productIdHex_edition[_otherIds]`.
|
||||||
|
pub const MSI_TABLE: &str = r#" |
||||||
|
14_4d463c2c-0505-4626-8cdb-a4da82e2d8ed_0015_AccessR |
||||||
|
14_745fb377-0a59-4ca9-b9a9-c359557a2c4e_001C_AccessRuntimeR |
||||||
|
14_95ab3ec8-4106-4f9d-b632-03c019d1d23f_0015_AccessVL |
||||||
|
14_4eaff0d0-c6cb-4187-94f3-c7656d49a0aa_0016_ExcelR_[HSExcelR] |
||||||
|
14_71dc86ff-f056-40d0-8ffb-9592705c9b76_0016_ExcelVL |
||||||
|
14_7004b7f0-6407-4f45-8eac-966e5f868bde_00BA_GrooveR |
||||||
|
14_fdad0dfa-417d-4b4f-93e4-64ea8867b7fd_00BA_GrooveVL |
||||||
|
14_7b7d1f17-fdcb-4820-9789-9bec6e377821_0013_HomeBusinessR_[HomeBusinessDemoR] |
||||||
|
14_19316117-30a8-4773-8fd9-7f7231f4e060_011E_HomeBusinessSubR |
||||||
|
14_09e2d37e-474b-4121-8626-58ad9be5776f_002F_HomeStudentR_[HomeStudentDemoR] |
||||||
|
14_ef1da464-01c8-43a6-91af-e4e5713744f9_0044_InfoPathR |
||||||
|
14_85e22450-b741-430c-a172-a37962c938af_0044_InfoPathVL |
||||||
|
14_14f5946a-debc-4716-babc-7e2c240fec08_000F_MondoR |
||||||
|
14_533b656a-4425-480b-8e30-1a2358898350_000F_MondoVL |
||||||
|
14_c1ceda8b-c578-4d5d-a4aa-23626be4e234_003D_ProfessionalR_[OEM-SingleImage]Exception |
||||||
|
14_3f7aa693-9a7e-44fc-9309-bb3d8e604925_00A1_OneNoteR_[HSOneNoteR] |
||||||
|
14_6860b31f-6a67-48b8-84b9-e312b3485c4b_00A1_OneNoteVL |
||||||
|
14_fbf4ac36-31c8-4340-8666-79873129cf40_001A_OutlookR |
||||||
|
14_a9aeabd8-63b8-4079-a28e-f531807fd6b8_001A_OutlookVL |
||||||
|
14_acb51361-c0db-4895-9497-1831c41f31a6_0033_PersonalR_[PersonalDemoR,PersonalPrepaidR] |
||||||
|
14_133c8359-4e93-4241-8118-30bb18737ea0_0018_PowerPointR_[HSPowerPointR] |
||||||
|
14_38252940-718c-4aa6-81a4-135398e53851_0018_PowerPointVL |
||||||
|
14_8b559c37-0117-413e-921b-b853aeb6e210_0014_ProfessionalR_[ProfessionalAcadR,ProfessionalDemoR] |
||||||
|
14_725714d7-d58f-4d12-9fa8-35873c6f7215_003B_ProjectProR_[ProjectProMSDNR] |
||||||
|
14_4d06f72e-fd50-4bc2-a24b-d448d7f17ef2_011F_ProjectProSubR |
||||||
|
14_1cf57a59-c532-4e56-9a7d-ffa2fe94b474_003B_ProjectProVL |
||||||
|
14_688f6589-2bd9-424e-a152-b13f36aa6de1_003A_ProjectStdR |
||||||
|
14_11b39439-6b93-4642-9570-f2eb81be2238_003A_ProjectStdVL |
||||||
|
14_71af7e84-93e6-4363-9b69-699e04e74071_0011_ProPlusR_[ProPlusAcadR,ProPlusMSDNR,Sub4R] |
||||||
|
14_e98ef0c0-71c4-42ce-8305-287d8721e26c_011D_ProPlusSubR |
||||||
|
14_fdf3ecb9-b56f-43b2-a9b8-1b48b6bae1a7_0011_ProPlusVL_[ProPlusAcadVL] |
||||||
|
14_98677603-a668-4fa4-9980-3f1f05f78f69_0019_PublisherR |
||||||
|
14_3d014759-b128-4466-9018-e80f6320d9d0_0019_PublisherVL |
||||||
|
14_dbe3aee0-5183-4ff7-8142-66050173cb01_008B_SmallBusBasicsR_[SmallBusBasicsMSDNR] |
||||||
|
14_8090771e-d41a-4482-929e-de87f1f47e46_008B_SmallBusBasicsVL |
||||||
|
14_b78df69e-0966-40b1-ae85-30a5134dedd0_0017_SPDR |
||||||
|
14_d3422cfb-8d8b-4ead-99f9-eab0ccd990d7_0012_StandardR |
||||||
|
14_1f76e346-e0be-49bc-9954-70ec53a4fcfe_0012_StandardVL_[StandardAcadVL] |
||||||
|
14_2745e581-565a-4670-ae90-6bf7c57ffe43_0066_StarterR |
||||||
|
14_66cad568-c2dc-459d-93ec-2f3cb967ee34_0057_VisioSIR_Prem[Pro,Std]Exception |
||||||
|
14_36756cb8-8e69-4d11-9522-68899507cd6a_0057_VisioSIVL_Prem[Pro,Std]Exception |
||||||
|
14_db3bbc9c-ce52-41d1-a46f-1a1d68059119_001B_WordR_[HSWordR] |
||||||
|
14_98d4050e-9c98-49bf-9be1-85e12eb3ab13_001B_WordVL |
||||||
|
:: Office 2013 |
||||||
|
15_ab4d047b-97cf-4126-a69f-34df08e2f254_0015_AccessRetail |
||||||
|
15_259de5be-492b-44b3-9d78-9645f848f7b0_001C_AccessRuntimeRetail |
||||||
|
15_4374022d-56b8-48c1-9bb7-d8f2fc726343_0015_AccessVolume |
||||||
|
15_1b1d9bd5-12ea-4063-964c-16e7e87d6e08_0016_ExcelRetail |
||||||
|
15_ac1ae7fd-b949-4e04-a330-849bc40638cf_0016_ExcelVolume |
||||||
|
15_cfaf5356-49e3-48a8-ab3c-e729ab791250_00BA_GrooveRetail |
||||||
|
15_4825ac28-ce41-45a7-9e6e-1fed74057601_00BA_GrooveVolume |
||||||
|
15_c02fb62e-1cd5-4e18-ba25-e0480467ffaa_00E7_HomeBusinessPipcRetail |
||||||
|
15_cd256150-a898-441f-aac0-9f8f33390e45_0013_HomeBusinessRetail |
||||||
|
15_1fdfb4e4-f9c9-41c4-b055-c80daf00697d_00CE_HomeStudentARMRetail |
||||||
|
15_ebef9f05-5273-404a-9253-c5e252f50555_00DA_HomeStudentPlusARMRetail |
||||||
|
15_98685d21-78bd-4c62-bc4f-653344a63035_002F_HomeStudentRetail |
||||||
|
15_44984381-406e-4a35-b1c3-e54f499556e2_0044_InfoPathRetail |
||||||
|
15_9e016989-4007-42a6-8051-64eb97110cf2_0044_InfoPathVolume |
||||||
|
15_9103f3ce-1084-447a-827e-d6097f68c895_00EA_LyncAcademicRetail |
||||||
|
15_ff693bf4-0276-4ddb-bb42-74ef1a0c9f4d_012D_LyncEntryRetail |
||||||
|
15_fada6658-bfc6-4c4e-825a-59a89822cda8_012C_LyncRetail |
||||||
|
15_e1264e10-afaf-4439-a98b-256df8bb156f_012C_LyncVolume |
||||||
|
15_3169c8df-f659-4f95-9cc6-3115e6596e83_000F_MondoRetail |
||||||
|
15_f33485a0-310b-4b72-9a0e-b1d605510dbd_000F_MondoVolume |
||||||
|
15_3391e125-f6e4-4b1e-899c-a25e6092d40d_00A1_OneNoteFreeRetail |
||||||
|
15_8b524bcc-67ea-4876-a509-45e46f6347e8_00A1_OneNoteRetail |
||||||
|
15_b067e965-7521-455b-b9f7-c740204578a2_00A1_OneNoteVolume |
||||||
|
15_12004b48-e6c8-4ffa-ad5a-ac8d4467765a_001A_OutlookRetail |
||||||
|
15_8d577c50-ae5e-47fd-a240-24986f73d503_001A_OutlookVolume |
||||||
|
15_5aab8561-1686-43f7-9ff5-2c861da58d17_00E6_PersonalPipcRetail |
||||||
|
15_17e9df2d-ed91-4382-904b-4fed6a12caf0_0033_PersonalRetail |
||||||
|
15_31743b82-bfbc-44b6-aa12-85d42e644d5b_0018_PowerPointRetail |
||||||
|
15_e40dcb44-1d5c-4085-8e8f-943f33c4f004_0018_PowerPointVolume |
||||||
|
15_4e26cac1-e15a-4467-9069-cb47b67fe191_00E8_ProfessionalPipcRetail |
||||||
|
15_44bc70e2-fb83-4b09-9082-e5557e0c2ede_0014_ProfessionalRetail |
||||||
|
15_f2435de4-5fc0-4e5b-ac97-34f515ec5ee7_003B_ProjectProRetail |
||||||
|
15_ed34dc89-1c27-4ecd-8b2f-63d0f4cedc32_003B_ProjectProVolume |
||||||
|
15_5517e6a2-739b-4822-946f-7f0f1c5934b1_003A_ProjectStdRetail |
||||||
|
15_2b9e4a37-6230-4b42-bee2-e25ce86c8c7a_003A_ProjectStdVolume |
||||||
|
15_064383fa-1538-491c-859b-0ecab169a0ab_0011_ProPlusRetail |
||||||
|
15_2b88c4f2-ea8f-43cd-805e-4d41346e18a7_0011_ProPlusVolume |
||||||
|
15_c3a0814a-70a4-471f-af37-2313a6331111_0019_PublisherRetail |
||||||
|
15_38ea49f6-ad1d-43f1-9888-99a35d7c9409_0019_PublisherVolume |
||||||
|
15_ba3e3833-6a7e-445a-89d0-7802a9a68588_0017_SPDRetail |
||||||
|
15_32255c0a-16b4-4ce2-b388-8a4267e219eb_0012_StandardRetail |
||||||
|
15_a24cca51-3d54-4c41-8a76-4031f5338cb2_0012_StandardVolume |
||||||
|
15_15d12ad4-622d-4257-976c-5eb3282fb93d_0051_VisioProRetail |
||||||
|
15_3e4294dd-a765-49bc-8dbd-cf8b62a4bd3d_0051_VisioProVolume |
||||||
|
15_dae597ce-5823-4c77-9580-7268b93a4b23_0053_VisioStdRetail |
||||||
|
15_44a1f6ff-0876-4edb-9169-dbb43101ee89_0053_VisioStdVolume |
||||||
|
15_191509f2-6977-456f-ab30-cf0492b1e93a_001B_WordRetail |
||||||
|
15_9cedef15-be37-4ff0-a08a-13a045540641_001B_WordVolume |
||||||
|
:: Office 365 - 15.0 version |
||||||
|
15_befee371-a2f5-4648-85db-a2c55fdf324c_00E9_O365BusinessRetail |
||||||
|
15_537ea5b5-7d50-4876-bd38-a53a77caca32_00D6_O365HomePremRetail |
||||||
|
15_149dbce7-a48e-44db-8364-a53386cd4580_00D4_O365ProPlusRetail |
||||||
|
15_bacd4614-5bef-4a5e-bafc-de4c788037a2_00D5_O365SmallBusPremRetail |
||||||
|
:: Office 365 - 16.0 version |
||||||
|
16_6337137e-7c07-4197-8986-bece6a76fc33_00E9_O365BusinessRetail |
||||||
|
16_2f5c71b4-5b7a-4005-bb68-f9fac26f2ea3_00D6_O365EduCloudRetail |
||||||
|
16_537ea5b5-7d50-4876-bd38-a53a77caca32_00D6_O365HomePremRetail |
||||||
|
16_149dbce7-a48e-44db-8364-a53386cd4580_00D4_O365ProPlusRetail |
||||||
|
16_bacd4614-5bef-4a5e-bafc-de4c788037a2_00D5_O365SmallBusPremRetail |
||||||
|
:: Office 2016 |
||||||
|
16_bfa358b0-98f1-4125-842e-585fa13032e6_0015_AccessRetail |
||||||
|
16_9d9faf9e-d345-4b49-afce-68cb0a539c7c_001C_AccessRuntimeRetail |
||||||
|
16_3b2fa33f-cd5a-43a5-bd95-f49f3f546b0b_0015_AccessVolume |
||||||
|
16_424d52ff-7ad2-4bc7-8ac6-748d767b455d_0016_ExcelRetail |
||||||
|
16_685062a7-6024-42e7-8c5f-6bb9e63e697f_0016_ExcelVolume |
||||||
|
16_c02fb62e-1cd5-4e18-ba25-e0480467ffaa_00E7_HomeBusinessPipcRetail |
||||||
|
16_86834d00-7896-4a38-8fae-32f20b86fa2b_0013_HomeBusinessRetail |
||||||
|
16_090896a0-ea98-48ac-b545-ba5da0eb0c9c_00CE_HomeStudentARMRetail |
||||||
|
16_6bbe2077-01a4-4269-bf15-5bf4d8efc0b2_00DA_HomeStudentPlusARMRetail |
||||||
|
16_c28acdb8-d8b3-4199-baa4-024d09e97c99_002F_HomeStudentRetail |
||||||
|
16_e2127526-b60c-43e0-bed1-3c9dc3d5a468_002F_HomeStudentVNextRetail |
||||||
|
16_b21367df-9545-4f02-9f24-240691da0e58_000F_MondoRetail |
||||||
|
16_2cd0ea7e-749f-4288-a05e-567c573b2a6c_000F_MondoVolume |
||||||
|
16_436366de-5579-4f24-96db-3893e4400030_00A3_OneNoteFreeRetail |
||||||
|
16_83ac4dd9-1b93-40ed-aa55-ede25bb6af38_00A1_OneNoteRetail |
||||||
|
16_23b672da-a456-4860-a8f3-e062a501d7e8_00A1_OneNoteVolume |
||||||
|
16_5a670809-0983-4c2d-8aad-d3c2c5b7d5d1_001A_OutlookRetail |
||||||
|
16_50059979-ac6f-4458-9e79-710bcb41721a_001A_OutlookVolume |
||||||
|
16_5aab8561-1686-43f7-9ff5-2c861da58d17_00E6_PersonalPipcRetail |
||||||
|
16_a9f645a1-0d6a-4978-926a-abcb363b72a6_0033_PersonalRetail |
||||||
|
16_f32d1284-0792-49da-9ac6-deb2bc9c80b6_0018_PowerPointRetail |
||||||
|
16_9b4060c9-a7f5-4a66-b732-faf248b7240f_0018_PowerPointVolume |
||||||
|
16_4e26cac1-e15a-4467-9069-cb47b67fe191_00E8_ProfessionalPipcRetail |
||||||
|
16_d64edc00-7453-4301-8428-197343fafb16_0014_ProfessionalRetail |
||||||
|
16_0f42f316-00b1-48c5-ada4-2f52b5720ad0_003B_ProjectProRetail |
||||||
|
16_82f502b5-b0b0-4349-bd2c-c560df85b248_003B_ProjectProVolume |
||||||
|
16_16728639-a9ab-4994-b6d8-f81051e69833_003B_ProjectProXVolume |
||||||
|
16_e9f0b3fc-962f-4944-ad06-05c10b6bcd5e_003A_ProjectStdRetail |
||||||
|
16_82e6b314-2a62-4e51-9220-61358dd230e6_003A_ProjectStdVolume |
||||||
|
16_431058f0-c059-44c5-b9e7-ed2dd46b6789_003A_ProjectStdXVolume |
||||||
|
16_de52bd50-9564-4adc-8fcb-a345c17f84f9_0011_ProPlusRetail |
||||||
|
16_c47456e3-265d-47b6-8ca0-c30abbd0ca36_0011_ProPlusVolume |
||||||
|
16_6e0c1d99-c72e-4968-bcb7-ab79e03e201e_0019_PublisherRetail |
||||||
|
16_fcc1757b-5d5f-486a-87cf-c4d6dedb6032_0019_PublisherVolume |
||||||
|
16_971cd368-f2e1-49c1-aedd-330909ce18b6_012D_SkypeforBusinessEntryRetail |
||||||
|
16_418d2b9f-b491-4d7f-84f1-49e27cc66597_012C_SkypeforBusinessRetail |
||||||
|
16_03ca3b9a-0869-4749-8988-3cbc9d9f51bb_012C_SkypeforBusinessVolume |
||||||
|
16_9103f3ce-1084-447a-827e-d6097f68c895_012C_SkypeServiceBypassRetail |
||||||
|
16_4a31c291-3a12-4c64-b8ab-cd79212be45e_0012_StandardRetail |
||||||
|
16_0ed94aac-2234-4309-ba29-74bdbb887083_0012_StandardVolume |
||||||
|
16_2dfe2075-2d04-4e43-816a-eb60bbb77574_0051_VisioProRetail |
||||||
|
16_295b2c03-4b1c-4221-b292-1411f468bd02_0051_VisioProVolume |
||||||
|
16_0594dc12-8444-4912-936a-747ca742dbdb_0051_VisioProXVolume |
||||||
|
16_c76dbcbc-d71b-4f45-b5b3-b7494cb4e23e_0053_VisioStdRetail |
||||||
|
16_44151c2d-c398-471f-946f-7660542e3369_0053_VisioStdVolume |
||||||
|
16_1d1c6879-39a3-47a5-9a6d-aceefa6a289d_0053_VisioStdXVolume |
||||||
|
16_cacaa1bf-da53-4c3b-9700-11738ef1c2a5_001B_WordRetail |
||||||
|
16_c3000759-551f-4f4a-bcac-a4b42cbf1de2_001B_WordVolume |
||||||
|
"#; |
||||||
@ -0,0 +1,304 @@ |
|||||||
|
//! `Separate-Files-Version/Extract_OEM_Folder.cmd` — build a `$OEM$` folder.
|
||||||
|
//!
|
||||||
|
//! The original script drops a Windows Setup `$OEM$` payload on the Desktop so
|
||||||
|
//! that activation runs automatically at OOBE. Windows Setup copies
|
||||||
|
//! `$OEM$\$$\...` into `%SystemRoot%\...`, so the payload lands at
|
||||||
|
//! `%SystemRoot%\Setup\Scripts\` and Setup runs `SetupComplete.cmd` there once,
|
||||||
|
//! post-install. The script's real work is: pick a combination of activators,
|
||||||
|
//! copy those activator `.cmd` files, and emit the matching `SetupComplete.cmd`.
|
||||||
|
//!
|
||||||
|
//! ## Portable (here, unit-tested)
|
||||||
|
//! * [`BuildOption`] → the exact `SetupComplete.cmd` bytes ([`BuildOption::setup_complete_cmd`]).
|
||||||
|
//! This is the heart of the script (the `:*_setup:` blocks + the `:export`
|
||||||
|
//! PowerShell that trims them and writes ASCII). Reproduced byte-faithfully,
|
||||||
|
//! CRLF line endings, no trailing newline — matching `.Trim()` output.
|
||||||
|
//! * The `$OEM$\$$\Setup\Scripts` folder layout + which activator files each
|
||||||
|
//! option needs ([`BuildOption::activator_files`]).
|
||||||
|
//!
|
||||||
|
//! ## Portable but effectful (here, `std::fs`)
|
||||||
|
//! * [`extract`] creates the folder tree, copies the activator `.cmd` files from
|
||||||
|
//! a caller-supplied source dir, and writes `SetupComplete.cmd`. No Windows
|
||||||
|
//! API — plain filesystem — so it lives in the module, not behind `Spp`.
|
||||||
|
//!
|
||||||
|
//! ## Windows-only (deferred to the caller / a new `Spp` method)
|
||||||
|
//! * Locating the Desktop (the script reads `User Shell Folders\Desktop` from
|
||||||
|
//! the registry, falling back to `%USERPROFILE%\Desktop`). That is a Windows
|
||||||
|
//! lookup, so it goes through `Spp::desktop_dir()` (see `new_spp_methods`);
|
||||||
|
//! [`extract`] just takes the destination path.
|
||||||
|
//! * All the batch bootstrapping (self-elevation, x64/ARM relaunch, QuickEdit,
|
||||||
|
//! PowerShell/AV sanity checks, update ping) is environment plumbing, not
|
||||||
|
//! part of the port.
|
||||||
|
|
||||||
|
use crate::error::Result; |
||||||
|
use std::io; |
||||||
|
use std::path::{Path, PathBuf}; |
||||||
|
|
||||||
|
/// Canonical activator script filenames (verbatim from the script's
|
||||||
|
/// `set <name>=Activators\<name>` table).
|
||||||
|
const HWID: &str = "HWID_Activation.cmd"; |
||||||
|
const OHOOK: &str = "Ohook_Activation_AIO.cmd"; |
||||||
|
const TSFORGE: &str = "TSforge_Activation.cmd"; |
||||||
|
const KMS: &str = "Online_KMS_Activation.cmd"; |
||||||
|
|
||||||
|
/// One `call "%~dp0<file>" <args>` line inside `SetupComplete.cmd`.
|
||||||
|
type Invocation = (&'static str, &'static str); |
||||||
|
|
||||||
|
/// The seven menu combinations the script offers.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||||
|
pub enum BuildOption { |
||||||
|
/// `[1]` HWID — Windows.
|
||||||
|
Hwid, |
||||||
|
/// `[2]` Ohook — Office.
|
||||||
|
Ohook, |
||||||
|
/// `[3]` TSforge — Windows / ESU / Office.
|
||||||
|
TSforge, |
||||||
|
/// `[4]` Online KMS — Windows / Office.
|
||||||
|
Kms, |
||||||
|
/// `[5]` HWID + Ohook.
|
||||||
|
HwidOhook, |
||||||
|
/// `[6]` HWID + Ohook + TSforge (ESU).
|
||||||
|
HwidOhookTsforge, |
||||||
|
/// `[7]` TSforge (Windows / ESU) + Ohook.
|
||||||
|
TsforgeOhook, |
||||||
|
} |
||||||
|
|
||||||
|
impl BuildOption { |
||||||
|
/// All options in menu order.
|
||||||
|
pub const ALL: [BuildOption; 7] = [ |
||||||
|
BuildOption::Hwid, |
||||||
|
BuildOption::Ohook, |
||||||
|
BuildOption::TSforge, |
||||||
|
BuildOption::Kms, |
||||||
|
BuildOption::HwidOhook, |
||||||
|
BuildOption::HwidOhookTsforge, |
||||||
|
BuildOption::TsforgeOhook, |
||||||
|
]; |
||||||
|
|
||||||
|
/// The menu hotkey digit (`choice /C:1234567R0`).
|
||||||
|
pub fn key(self) -> char { |
||||||
|
match self { |
||||||
|
BuildOption::Hwid => '1', |
||||||
|
BuildOption::Ohook => '2', |
||||||
|
BuildOption::TSforge => '3', |
||||||
|
BuildOption::Kms => '4', |
||||||
|
BuildOption::HwidOhook => '5', |
||||||
|
BuildOption::HwidOhookTsforge => '6', |
||||||
|
BuildOption::TsforgeOhook => '7', |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The success label (verbatim from the script's `set oem=` lines).
|
||||||
|
pub fn label(self) -> &'static str { |
||||||
|
match self { |
||||||
|
BuildOption::Hwid => "HWID", |
||||||
|
BuildOption::Ohook => "Ohook", |
||||||
|
BuildOption::TSforge => "TSforge", |
||||||
|
BuildOption::Kms => "Online KMS", |
||||||
|
BuildOption::HwidOhook => "HWID [Windows] + Ohook [Office]", |
||||||
|
BuildOption::HwidOhookTsforge => "HWID [Windows] + Ohook [Office] + TSforge [ESU]", |
||||||
|
BuildOption::TsforgeOhook => "TSforge [Windows / ESU] + Ohook [Office]", |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The activator calls this option runs, in order, as `(filename, args)`.
|
||||||
|
/// Args are transcribed verbatim from the `:*_setup:` blocks.
|
||||||
|
pub fn invocations(self) -> Vec<Invocation> { |
||||||
|
match self { |
||||||
|
BuildOption::Hwid => vec![(HWID, "/HWID")], |
||||||
|
BuildOption::Ohook => vec![(OHOOK, "/Ohook")], |
||||||
|
BuildOption::TSforge => vec![(TSFORGE, "/Z-WindowsESUOffice")], |
||||||
|
BuildOption::Kms => vec![(KMS, "/K-WindowsOffice")], |
||||||
|
BuildOption::HwidOhook => vec![(HWID, "/HWID"), (OHOOK, "/Ohook")], |
||||||
|
BuildOption::HwidOhookTsforge => { |
||||||
|
vec![(HWID, "/HWID"), (OHOOK, "/Ohook"), (TSFORGE, "/Z-ESU")] |
||||||
|
} |
||||||
|
BuildOption::TsforgeOhook => { |
||||||
|
vec![(TSFORGE, "/Z-Windows /Z-ESU"), (OHOOK, "/Ohook")] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Distinct activator files this option copies into the payload, in order.
|
||||||
|
pub fn activator_files(self) -> Vec<&'static str> { |
||||||
|
let mut files = Vec::new(); |
||||||
|
for (f, _) in self.invocations() { |
||||||
|
if !files.contains(&f) { |
||||||
|
files.push(f); |
||||||
|
} |
||||||
|
} |
||||||
|
files |
||||||
|
} |
||||||
|
|
||||||
|
/// The exact `SetupComplete.cmd` text for this option.
|
||||||
|
///
|
||||||
|
/// Byte-faithful to what the batch `:export` step produced: the trimmed
|
||||||
|
/// `:*_setup:` block, CRLF line endings, ASCII, no trailing newline. A
|
||||||
|
/// single activator is called directly; multiple are each wrapped in a
|
||||||
|
/// `setlocal`/`endlocal` pair (as the combo blocks do).
|
||||||
|
pub fn setup_complete_cmd(self) -> String { |
||||||
|
let mut lines: Vec<String> = vec![ |
||||||
|
"@echo off".into(), |
||||||
|
String::new(), |
||||||
|
"fltmc >nul || exit /b".into(), |
||||||
|
String::new(), |
||||||
|
]; |
||||||
|
|
||||||
|
let invs = self.invocations(); |
||||||
|
if invs.len() == 1 { |
||||||
|
let (file, args) = invs[0]; |
||||||
|
lines.push(format!("call \"%~dp0{file}\" {args}")); |
||||||
|
} else { |
||||||
|
for (i, (file, args)) in invs.iter().enumerate() { |
||||||
|
if i > 0 { |
||||||
|
lines.push(String::new()); |
||||||
|
} |
||||||
|
lines.push("setlocal".into()); |
||||||
|
lines.push(format!("call \"%~dp0{file}\" {args}")); |
||||||
|
lines.push("endlocal".into()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
lines.push(String::new()); |
||||||
|
lines.push("cd \\".into()); |
||||||
|
lines.push( |
||||||
|
r#"(goto) 2>nul & (if "%~dp0"=="%SystemRoot%\Setup\Scripts\" rd /s /q "%~dp0")"#.into(), |
||||||
|
); |
||||||
|
|
||||||
|
lines.join("\r\n") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Relative payload path under the Desktop: `$OEM$\$$\Setup\Scripts`.
|
||||||
|
///
|
||||||
|
/// Windows Setup strips the leading `$OEM$\$$` and copies the rest into
|
||||||
|
/// `%SystemRoot%`, so this ends up at `%SystemRoot%\Setup\Scripts`.
|
||||||
|
fn scripts_dir(dest_root: &Path) -> PathBuf { |
||||||
|
dest_root |
||||||
|
.join("$OEM$") |
||||||
|
.join("$$") |
||||||
|
.join("Setup") |
||||||
|
.join("Scripts") |
||||||
|
} |
||||||
|
|
||||||
|
/// Build the `$OEM$` payload under `dest_root` (the Desktop).
|
||||||
|
///
|
||||||
|
/// `activators_src` is the folder holding the activator `.cmd` files (the
|
||||||
|
/// script's `Activators\` directory). Returns the created `...\Setup\Scripts`
|
||||||
|
/// directory. Mirrors the script's guards: refuse if `$OEM$` already exists,
|
||||||
|
/// and fail up front if a required activator file is missing.
|
||||||
|
pub fn extract(option: BuildOption, activators_src: &Path, dest_root: &Path) -> Result<PathBuf> { |
||||||
|
let oem_root = dest_root.join("$OEM$"); |
||||||
|
if oem_root.exists() { |
||||||
|
return Err(io::Error::new( |
||||||
|
io::ErrorKind::AlreadyExists, |
||||||
|
"The $OEM$ folder already exists on your Desktop.", |
||||||
|
) |
||||||
|
.into()); |
||||||
|
} |
||||||
|
|
||||||
|
// Faithful to the `_nofile` pre-check: verify sources before touching disk.
|
||||||
|
let files = option.activator_files(); |
||||||
|
for f in &files { |
||||||
|
let src = activators_src.join(f); |
||||||
|
if !src.is_file() { |
||||||
|
return Err(io::Error::new( |
||||||
|
io::ErrorKind::NotFound, |
||||||
|
format!("Missing file in the 'Activators' folder: {f}"), |
||||||
|
) |
||||||
|
.into()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let scripts = scripts_dir(dest_root); |
||||||
|
std::fs::create_dir_all(&scripts).map_err(crate::error::Error::Io)?; |
||||||
|
|
||||||
|
for f in &files { |
||||||
|
std::fs::copy(activators_src.join(f), scripts.join(f)).map_err(crate::error::Error::Io)?; |
||||||
|
} |
||||||
|
|
||||||
|
// ASCII by construction, so bytes == what `WriteAllText(..., ASCII)` emitted.
|
||||||
|
std::fs::write(scripts.join("SetupComplete.cmd"), option.setup_complete_cmd()) |
||||||
|
.map_err(crate::error::Error::Io)?; |
||||||
|
|
||||||
|
Ok(scripts) |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn hwid_setup_is_byte_faithful() { |
||||||
|
let expected = "@echo off\r\n\ |
||||||
|
\r\n\ |
||||||
|
fltmc >nul || exit /b\r\n\ |
||||||
|
\r\n\ |
||||||
|
call \"%~dp0HWID_Activation.cmd\" /HWID\r\n\ |
||||||
|
\r\n\ |
||||||
|
cd \\\r\n\ |
||||||
|
(goto) 2>nul & (if \"%~dp0\"==\"%SystemRoot%\\Setup\\Scripts\\\" rd /s /q \"%~dp0\")"; |
||||||
|
assert_eq!(BuildOption::Hwid.setup_complete_cmd(), expected); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn tsforge_office_uses_combined_switch() { |
||||||
|
// Single-activator TSforge takes the combined /Z-WindowsESUOffice switch.
|
||||||
|
assert!(BuildOption::TSforge |
||||||
|
.setup_complete_cmd() |
||||||
|
.contains("call \"%~dp0TSforge_Activation.cmd\" /Z-WindowsESUOffice")); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn tsforge_ohook_setup_is_byte_faithful() { |
||||||
|
let expected = "@echo off\r\n\ |
||||||
|
\r\n\ |
||||||
|
fltmc >nul || exit /b\r\n\ |
||||||
|
\r\n\ |
||||||
|
setlocal\r\n\ |
||||||
|
call \"%~dp0TSforge_Activation.cmd\" /Z-Windows /Z-ESU\r\n\ |
||||||
|
endlocal\r\n\ |
||||||
|
\r\n\ |
||||||
|
setlocal\r\n\ |
||||||
|
call \"%~dp0Ohook_Activation_AIO.cmd\" /Ohook\r\n\ |
||||||
|
endlocal\r\n\ |
||||||
|
\r\n\ |
||||||
|
cd \\\r\n\ |
||||||
|
(goto) 2>nul & (if \"%~dp0\"==\"%SystemRoot%\\Setup\\Scripts\\\" rd /s /q \"%~dp0\")"; |
||||||
|
assert_eq!(BuildOption::TsforgeOhook.setup_complete_cmd(), expected); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn combo_blocks_wrap_every_call_in_setlocal() { |
||||||
|
let text = BuildOption::HwidOhookTsforge.setup_complete_cmd(); |
||||||
|
assert_eq!(text.matches("setlocal\r\n").count(), 3); |
||||||
|
assert_eq!(text.matches("\r\nendlocal").count(), 3); |
||||||
|
assert!(text.contains("call \"%~dp0TSforge_Activation.cmd\" /Z-ESU")); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn output_is_ascii_and_has_no_trailing_newline() { |
||||||
|
for opt in BuildOption::ALL { |
||||||
|
let s = opt.setup_complete_cmd(); |
||||||
|
assert!(s.is_ascii(), "{:?} not ASCII", opt); |
||||||
|
assert!(!s.ends_with('\n'), "{:?} has trailing newline", opt); |
||||||
|
assert!(s.starts_with("@echo off\r\n")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn activator_files_are_distinct_and_ordered() { |
||||||
|
assert_eq!(BuildOption::Hwid.activator_files(), vec![HWID]); |
||||||
|
assert_eq!( |
||||||
|
BuildOption::HwidOhookTsforge.activator_files(), |
||||||
|
vec![HWID, OHOOK, TSFORGE] |
||||||
|
); |
||||||
|
// Ohook appears once even though it is only ever one call.
|
||||||
|
assert_eq!(BuildOption::TsforgeOhook.activator_files(), vec![TSFORGE, OHOOK]); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn keys_match_the_menu() { |
||||||
|
let keys: Vec<char> = BuildOption::ALL.iter().map(|o| o.key()).collect(); |
||||||
|
assert_eq!(keys, vec!['1', '2', '3', '4', '5', '6', '7']); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
//! Portable utilities ported from the standalone MAS scripts.
|
||||||
|
|
||||||
|
pub mod troubleshoot; |
||||||
@ -0,0 +1,314 @@ |
|||||||
|
//! `Troubleshoot.cmd` — activation/licensing repair menu.
|
||||||
|
//!
|
||||||
|
//! Source: `MAS/Separate-Files-Version/Troubleshoot.cmd` (git `f34d025`).
|
||||||
|
//!
|
||||||
|
//! The script offers six menu options; each maps to a [`TroubleshootAction`].
|
||||||
|
//! Only a small amount of the script is *decision* logic — the rest is Windows
|
||||||
|
//! plumbing (stopping `sppsvc`/`ClipSVC`/`osppsvc`, deleting `tokens.dat`,
|
||||||
|
//! rebuilding the token stores, running `dism`/`sfc`, rebuilding the WMI
|
||||||
|
//! repository, repairing Office). That plumbing crosses the [`Spp`] boundary.
|
||||||
|
//!
|
||||||
|
//! Portable + unit-tested here:
|
||||||
|
//! * The ~31-row HWID (retail digital-license) partial-key list and the
|
||||||
|
//! case-insensitive [`is_hwid_key`] matcher (script `:cleanlicensing`).
|
||||||
|
//! * The three SPP/OSPP `ApplicationID` GUIDs ([`APPID_WINDOWS`],
|
||||||
|
//! [`APPID_OFFICE`], [`APPID_OFFICE_2010`]).
|
||||||
|
//! * The `tokens.dat` store path templates ([`SPP_TOKEN_STORES`],
|
||||||
|
//! [`OSPP_TOKEN_STORES`]) + [`expand_token_store`], and the valid
|
||||||
|
//! `TokenStore` registry values ([`VALID_TOKEN_STORES`]).
|
||||||
|
//! * The ClipSVC-rebuild gate [`should_rebuild_clipsvc`] and the per-action
|
||||||
|
//! build guards in [`TroubleshootAction::run`].
|
||||||
|
//!
|
||||||
|
//! Windows-only (behind the [`Spp`] trait, added via `new_spp_methods`):
|
||||||
|
//! `dism_restore_health`, `sfc_scannow`, `rebuild_wmi_repository`,
|
||||||
|
//! `reset_clipsvc_store`, `reset_spp_store`, `reset_ospp_store`, `repair_office`.
|
||||||
|
//!
|
||||||
|
//! Deferred (not ported): opening the support web pages for `Help` /
|
||||||
|
//! `Fix WPA Registry` (a CLI/browser concern), CBS/DISM log compression to the
|
||||||
|
//! desktop, the SDDL/registry permission-repair (`:fixsppperms`) and the
|
||||||
|
//! volatile-key ownership dance (`:regown`) — those live inside the coarse
|
||||||
|
//! `reset_*` effects on the backend.
|
||||||
|
|
||||||
|
use crate::error::{Error, Result}; |
||||||
|
use crate::model::Product; |
||||||
|
use crate::platform::{LicenseInfo, Spp}; |
||||||
|
|
||||||
|
/// SPP `ApplicationID` for Windows (`_wApp`).
|
||||||
|
pub const APPID_WINDOWS: &str = "55c92734-d682-4d71-983e-d6ec3f16059f"; |
||||||
|
/// SPP `ApplicationID` for Office 2013+ / C2R (`_oApp`).
|
||||||
|
pub const APPID_OFFICE: &str = "0ff1ce15-a989-479d-af46-f275c6370663"; |
||||||
|
/// OSPP `ApplicationID` for Office 2010 (`_oA14`).
|
||||||
|
pub const APPID_OFFICE_2010: &str = "59a52881-a989-479d-af46-f275c6370663"; |
||||||
|
|
||||||
|
/// The 31 HWID (retail digital-license) partial product keys, verbatim from the
|
||||||
|
/// script's `:cleanlicensing` match list. These are the last-five-character
|
||||||
|
/// `PartialProductKey` values Windows reports for a store/digital-license key;
|
||||||
|
/// their presence means a ClipSVC rebuild is worthwhile.
|
||||||
|
pub const HWID_PARTIAL_KEYS: &[&str] = &[ |
||||||
|
"8HV2C", "QPFCT", "3V66T", "PKCKT", "WXCHW", "8TYMD", "6F4BT", "8HVX7", "KD72Y", "7CFBY", |
||||||
|
"DRR8H", "P39PB", "DYJWX", "MDWWW", "9HKR4", "M7V2X", "2YV77", "WT2RQ", "MHBPB", "QPF8P", |
||||||
|
"2YV66", "VMJ2C", "DJ4F6", "CKFFD", "YY74H", "J8JXD", "BHDCD", "T6R4W", "D32MH", "RRK69", |
||||||
|
"3PJBP", |
||||||
|
]; |
||||||
|
|
||||||
|
/// Case-insensitive membership test against [`HWID_PARTIAL_KEYS`]
|
||||||
|
/// (script: `if /i "%_partial%"=="%%#"`).
|
||||||
|
pub fn is_hwid_key(partial: &str) -> bool { |
||||||
|
let partial = partial.trim(); |
||||||
|
HWID_PARTIAL_KEYS |
||||||
|
.iter() |
||||||
|
.any(|k| k.eq_ignore_ascii_case(partial)) |
||||||
|
} |
||||||
|
|
||||||
|
/// SPP `tokens.dat` store directory templates scanned by the script's `:scandat`
|
||||||
|
/// (order preserved). Substitute the placeholders with [`expand_token_store`].
|
||||||
|
pub const SPP_TOKEN_STORES: &[&str] = &[ |
||||||
|
r"%SysPath%\spp\store_test\2.0\", |
||||||
|
r"%SysPath%\spp\store\", |
||||||
|
r"%SysPath%\spp\store\2.0\", |
||||||
|
r"%Systemdrive%\Windows\ServiceProfiles\NetworkService\AppData\Roaming\Microsoft\SoftwareProtectionPlatform\", |
||||||
|
r"%Systemdrive%\Windows\ServiceProfiles\NetworkService\AppData\Roaming\Microsoft\SoftwareLicensing\", |
||||||
|
]; |
||||||
|
|
||||||
|
/// OSPP `tokens.dat` store directory template (`:scandatospp`).
|
||||||
|
pub const OSPP_TOKEN_STORES: &[&str] = &[r"%ProgramData%\Microsoft\OfficeSoftwareProtectionPlatform\"]; |
||||||
|
|
||||||
|
/// Values the `TokenStore` registry entry is allowed to hold (`%SysPath%`
|
||||||
|
/// still to be expanded); anything else the script flags as a bad registry.
|
||||||
|
pub const VALID_TOKEN_STORES: &[&str] = &[ |
||||||
|
r"%SysPath%\spp\store", |
||||||
|
r"%SysPath%\spp\store\2.0", |
||||||
|
r"%SysPath%\spp\store_test\2.0", |
||||||
|
]; |
||||||
|
|
||||||
|
/// Expand a store template's `%SysPath%` / `%Systemdrive%` / `%ProgramData%`
|
||||||
|
/// placeholders. Pure so the backend and tests share one substitution.
|
||||||
|
pub fn expand_token_store(template: &str, sys_path: &str, system_drive: &str, program_data: &str) -> String { |
||||||
|
template |
||||||
|
.replace("%SysPath%", sys_path) |
||||||
|
.replace("%Systemdrive%", system_drive) |
||||||
|
.replace("%ProgramData%", program_data) |
||||||
|
} |
||||||
|
|
||||||
|
/// Whether the ClipSVC license rebuild sub-flow should run (script
|
||||||
|
/// `:cleanlicensing` gate): Windows 10+ (`>= 10240`), not already permanently
|
||||||
|
/// activated, and an HWID/digital-license key is installed. The internet /
|
||||||
|
/// licensing-server reachability checks that follow are Windows effects and
|
||||||
|
/// live inside [`Spp::reset_clipsvc_store`].
|
||||||
|
pub fn should_rebuild_clipsvc(build: u32, windows_products: &[LicenseInfo]) -> bool { |
||||||
|
if build < 10240 { |
||||||
|
return false; |
||||||
|
} |
||||||
|
// Permanently activated = Licensed with no grace remaining (OEM/retail/
|
||||||
|
// digital); the script skips the rebuild in that case.
|
||||||
|
let permanent = windows_products |
||||||
|
.iter() |
||||||
|
.any(|p| p.is_activated() && p.grace_minutes == Some(0)); |
||||||
|
if permanent { |
||||||
|
return false; |
||||||
|
} |
||||||
|
windows_products |
||||||
|
.iter() |
||||||
|
.any(|p| p.partial_product_key.as_deref().is_some_and(is_hwid_key)) |
||||||
|
} |
||||||
|
|
||||||
|
/// The Troubleshoot menu options (`:at_menu`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||||
|
pub enum TroubleshootAction { |
||||||
|
/// `[1]` Open the online help / troubleshooting pages (informational).
|
||||||
|
Help, |
||||||
|
/// `[2]` `dism /english /online /cleanup-image /restorehealth`.
|
||||||
|
DismRestoreHealth, |
||||||
|
/// `[3]` `sfc /scannow`.
|
||||||
|
SfcScannow, |
||||||
|
/// `[4]` Rebuild the WMI repository.
|
||||||
|
FixWmi, |
||||||
|
/// `[5]` Fix Licensing (ClipSVC + SPP + OSPP + Office repair).
|
||||||
|
FixLicensing, |
||||||
|
/// `[6]` Open the WPA-registry-fix page (informational).
|
||||||
|
FixWpaRegistry, |
||||||
|
} |
||||||
|
|
||||||
|
impl TroubleshootAction { |
||||||
|
/// Menu order, matching the script.
|
||||||
|
pub const ALL: [TroubleshootAction; 6] = [ |
||||||
|
TroubleshootAction::Help, |
||||||
|
TroubleshootAction::DismRestoreHealth, |
||||||
|
TroubleshootAction::SfcScannow, |
||||||
|
TroubleshootAction::FixWmi, |
||||||
|
TroubleshootAction::FixLicensing, |
||||||
|
TroubleshootAction::FixWpaRegistry, |
||||||
|
]; |
||||||
|
|
||||||
|
pub const fn label(self) -> &'static str { |
||||||
|
match self { |
||||||
|
TroubleshootAction::Help => "Help", |
||||||
|
TroubleshootAction::DismRestoreHealth => "Dism RestoreHealth", |
||||||
|
TroubleshootAction::SfcScannow => "SFC Scannow", |
||||||
|
TroubleshootAction::FixWmi => "Fix WMI", |
||||||
|
TroubleshootAction::FixLicensing => "Fix Licensing", |
||||||
|
TroubleshootAction::FixWpaRegistry => "Fix WPA Registry", |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Whether the action only opens a support web page (no local effect).
|
||||||
|
pub const fn is_informational(self) -> bool { |
||||||
|
matches!(self, TroubleshootAction::Help | TroubleshootAction::FixWpaRegistry) |
||||||
|
} |
||||||
|
|
||||||
|
/// Run the action. Requires elevation; applies the script's build guards
|
||||||
|
/// before delegating each effect across the [`Spp`] boundary.
|
||||||
|
pub fn run(self, spp: &dyn Spp) -> Result<()> { |
||||||
|
if self.is_informational() { |
||||||
|
// Help / Fix WPA Registry only launch a browser; the CLI handles
|
||||||
|
// that, the core has nothing to do.
|
||||||
|
return Ok(()); |
||||||
|
} |
||||||
|
if !spp.is_elevated() { |
||||||
|
return Err(Error::NotElevated); |
||||||
|
} |
||||||
|
match self { |
||||||
|
TroubleshootAction::DismRestoreHealth => { |
||||||
|
if spp.windows_build()? < 9200 { |
||||||
|
return Err(unsupported("DISM RestoreHealth requires Windows 8/8.1/10/11 or Server equivalents")); |
||||||
|
} |
||||||
|
spp.dism_restore_health() |
||||||
|
} |
||||||
|
TroubleshootAction::SfcScannow => spp.sfc_scannow(), |
||||||
|
TroubleshootAction::FixWmi => spp.rebuild_wmi_repository(), |
||||||
|
TroubleshootAction::FixLicensing => fix_licensing(spp), |
||||||
|
TroubleshootAction::Help | TroubleshootAction::FixWpaRegistry => unreachable!(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Fix Licensing flow (`:retokens`): optional ClipSVC rebuild (gated), then
|
||||||
|
/// SPP + OSPP token-store rebuilds, then the Office repair trigger. The gate is
|
||||||
|
/// portable; each rebuild is a coarse Windows effect.
|
||||||
|
fn fix_licensing(spp: &dyn Spp) -> Result<()> { |
||||||
|
let build = spp.windows_build()?; |
||||||
|
if build == 6001 { |
||||||
|
return Err(unsupported("Fix Licensing is not supported on Windows Vista SP1; upgrade to SP2")); |
||||||
|
} |
||||||
|
if should_rebuild_clipsvc(build, &spp.installed_products(Product::Windows)?) { |
||||||
|
spp.reset_clipsvc_store()?; |
||||||
|
} |
||||||
|
spp.reset_spp_store()?; |
||||||
|
spp.reset_ospp_store()?; |
||||||
|
spp.repair_office()?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn unsupported(what: &str) -> Error { |
||||||
|
Error::Unsupported { what: what.into() } |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use crate::model::LicenseStatus; |
||||||
|
use crate::platform::test_util::FakeSpp; |
||||||
|
|
||||||
|
fn win(key: Option<&str>, activated: bool, grace: Option<u32>) -> LicenseInfo { |
||||||
|
LicenseInfo { |
||||||
|
activation_id: "win".into(), |
||||||
|
name: "Windows".into(), |
||||||
|
description: String::new(), |
||||||
|
partial_product_key: key.map(str::to_string), |
||||||
|
status: LicenseStatus::from_wmi(if activated { 1 } else { 0 }).unwrap(), |
||||||
|
license_family: None, |
||||||
|
grace_minutes: grace, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn hwid_list_is_verbatim() { |
||||||
|
assert_eq!(HWID_PARTIAL_KEYS.len(), 31); |
||||||
|
assert_eq!(HWID_PARTIAL_KEYS[0], "8HV2C"); |
||||||
|
assert_eq!(HWID_PARTIAL_KEYS[30], "3PJBP"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn is_hwid_key_matches_case_insensitively() { |
||||||
|
assert!(is_hwid_key("8HV2C")); |
||||||
|
assert!(is_hwid_key("8hv2c")); // case-insensitive
|
||||||
|
assert!(is_hwid_key(" 3PJBP ")); // trimmed
|
||||||
|
assert!(!is_hwid_key("ABCDE")); |
||||||
|
assert!(!is_hwid_key("")); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn appid_guids_are_verbatim() { |
||||||
|
assert_eq!(APPID_WINDOWS, "55c92734-d682-4d71-983e-d6ec3f16059f"); |
||||||
|
assert_eq!(APPID_OFFICE, "0ff1ce15-a989-479d-af46-f275c6370663"); |
||||||
|
assert_eq!(APPID_OFFICE_2010, "59a52881-a989-479d-af46-f275c6370663"); |
||||||
|
// Windows AppID must agree with the domain model's constant.
|
||||||
|
assert_eq!(APPID_WINDOWS, Product::Windows.application_id()); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn expand_token_store_substitutes_placeholders() { |
||||||
|
let got = expand_token_store( |
||||||
|
r"%SysPath%\spp\store\", |
||||||
|
r"C:\Windows\System32", |
||||||
|
"C:", |
||||||
|
r"C:\ProgramData", |
||||||
|
); |
||||||
|
assert_eq!(got, r"C:\Windows\System32\spp\store\"); |
||||||
|
let ospp = expand_token_store(OSPP_TOKEN_STORES[0], "sp", "sd", "PD"); |
||||||
|
assert_eq!(ospp, r"PD\Microsoft\OfficeSoftwareProtectionPlatform\"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn clipsvc_gate() { |
||||||
|
// Too old.
|
||||||
|
assert!(!should_rebuild_clipsvc(7601, &[win(Some("8HV2C"), false, None)])); |
||||||
|
// HWID key present, not permanently activated -> rebuild.
|
||||||
|
assert!(should_rebuild_clipsvc(19045, &[win(Some("8HV2C"), false, None)])); |
||||||
|
// Permanently activated (Licensed, grace 0) -> skip.
|
||||||
|
assert!(!should_rebuild_clipsvc(19045, &[win(Some("8HV2C"), true, Some(0))])); |
||||||
|
// No HWID key installed -> skip.
|
||||||
|
assert!(!should_rebuild_clipsvc(19045, &[win(Some("XXXXX"), false, None)])); |
||||||
|
// No products at all -> skip.
|
||||||
|
assert!(!should_rebuild_clipsvc(19045, &[])); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn action_menu_shape() { |
||||||
|
assert_eq!(TroubleshootAction::ALL.len(), 6); |
||||||
|
assert_eq!(TroubleshootAction::DismRestoreHealth.label(), "Dism RestoreHealth"); |
||||||
|
assert!(TroubleshootAction::Help.is_informational()); |
||||||
|
assert!(!TroubleshootAction::FixLicensing.is_informational()); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn informational_actions_are_noops() { |
||||||
|
let spp = FakeSpp::default(); |
||||||
|
TroubleshootAction::Help.run(&spp).unwrap(); |
||||||
|
TroubleshootAction::FixWpaRegistry.run(&spp).unwrap(); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn effect_actions_require_elevation() { |
||||||
|
let spp = FakeSpp { elevated: false, ..Default::default() }; |
||||||
|
assert!(matches!(TroubleshootAction::SfcScannow.run(&spp), Err(Error::NotElevated))); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn dism_guard_rejects_old_builds() { |
||||||
|
let spp = FakeSpp { build: 7601, ..Default::default() }; |
||||||
|
assert!(matches!( |
||||||
|
TroubleshootAction::DismRestoreHealth.run(&spp), |
||||||
|
Err(Error::Unsupported { .. }) |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn fix_licensing_rejects_vista_sp1() { |
||||||
|
let spp = FakeSpp { build: 6001, ..Default::default() }; |
||||||
|
assert!(matches!( |
||||||
|
TroubleshootAction::FixLicensing.run(&spp), |
||||||
|
Err(Error::Unsupported { .. }) |
||||||
|
)); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue