La cause n’était finalement ni la licence du tenant, ni l’absence de jointure Entra, ni l’horloge, ni l’installation Click-to-Run. Le journal AAD révélait une clé protégée du Web Account Manager (WAM) devenue incohérente avec son parent de stockage TPM :

Sortie
0xCAA5001C Token broker operation failed
GetTokenSilently
0xCAA5004B Device information is missing
0x8029040E TPM_E_PCP_WRONG_PARENT
NgcHelper::SignWithSymmetricPopKey

La correction effective a été une réinitialisation du package utilisateur Microsoft.AAD.BrokerPlugin, après sauvegarde complète et vérifiée. Le TPM n’a pas été effacé. Après reconnexion manuelle dans Word, la licence est passée à O365BusinessRetail = vNext, Licensed, et Outlook ainsi que OneDrive se sont reconnectés.

Cet article présente une méthode progressive. Commencez toujours par les diagnostics en lecture seule. N’effacez jamais le TPM ou un cache d’identité sans sauvegarde et sans comprendre l’impact sur BitLocker, Windows Hello et les autres comptes.

Conclusion technique confirmée

La cause confirmée était un état utilisateur WAM/AAD Broker contenant une clé protégée incohérente avec son parent TPM. Le TPM matériel, l’absence de jointure Entra, DeployedInAuditMode et O365BusinessRetail.TenantId = "1," n’étaient pas la cause.

Résumé de la résolution

ÉlémentAvantAprès
ProduitO365BusinessRetailO365BusinessRetail
Mode d’activationLegacyvNext
Licence vNextAucuneUser|Subscription, Licensed
Fichiers dans %LOCALAPPDATA%\Microsoft\Office\Licenses01
Erreur racine0x8029040E répétée0 après correction
Jointure EntraNonNon, inchangée
Jointure AD localeOuiOui, inchangée
TPMSain et opérationnelInchangé, jamais effacé
Word / Outlook / OneDriveNon licenciés ou bloquésConnectés et opérationnels

Environnement du cas

Les identifiants ont été anonymisés.

  • Windows 11 Pro, build 10.0.26200 ;
  • poste joint à un domaine Active Directory local ;
  • DomainJoined = YES ;
  • AzureAdJoined = NO ;
  • WorkplaceJoined = NO ;
  • AzureAdPrt = NO ;
  • Microsoft 365 Apps for business ;
  • ProductReleaseIds = OneNoteFreeRetail,O365BusinessRetail ;
  • utilisateur licencié Microsoft 365 Business Premium ;
  • aucun contrôleur de domaine joignable pendant l’intervention, le poste étant hors site ;
  • UPN Active Directory différent de l’adresse de connexion Microsoft 365.

La différence entre l’UPN AD local et l’adresse Microsoft 365 constitue une dette d’hygiène d’identité, surtout lorsque mail et proxyAddresses sont vides. Elle n’était toutefois pas la cause bloquante : la licence a été acquise sans modifier l’UPN AD.

Symptômes

Les symptômes observés pendant quatre jours étaient les suivants :

  • Word : « Consultation uniquement (sans licence) » ;
  • Office : « Produit sans licence » ;
  • Word et Outlook ouverts en période de grâce ;
  • fenêtre Microsoft « Une erreur s’est produite » ;
  • Tag 7ita9 ;
  • code 3399811147, soit 0xCAA5004B ;
  • environ 700 événements AAD en 24 heures ;
  • aucune licence dans le cache vNext.

La licence avait été vérifiée dans le centre d’administration Microsoft 365 : elle était attribuée à l’utilisateur et ses plans de service étaient actifs.

Diagnostics à effectuer avant toute modification

1. Identifier le produit Click-to-Run

PowerShell
$c2r = 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration'
Get-ItemProperty -LiteralPath $c2r |
  Select-Object ProductReleaseIds,VersionToReport,Platform,
    'O365BusinessRetail.TenantId',
    'O365BusinessRetail.OSPPReady',
    DeployedInAuditMode |
  Format-List

Dans ce cas :

Sortie
ProductReleaseIds            : OneNoteFreeRetail,O365BusinessRetail
O365BusinessRetail.TenantId  : 1,
O365BusinessRetail.OSPPReady : 1
DeployedInAuditMode          : True

2. Contrôler la licence vNext

Depuis Office 2104, Microsoft fournit vNextDiag.ps1 pour inspecter l’activation moderne. Pour Microsoft 365 Apps, cet outil est plus pertinent qu’ospp.vbs, réservé principalement aux éditions en licence en volume.

PowerShell
& 'C:\Program Files\Microsoft Office\root\Office16\vNextDiag.ps1' -action list

État en échec :

Sortie
========== Mode per ProductReleaseId ==========
o365businessretail = Legacy

========== vNext licenses found ==========
No licenses found.

État après résolution, anonymisé :

Sortie
========== Mode per ProductReleaseId ==========
o365businessretail = vNext

{
  "Type": "User|Subscription",
  "Product": "O365BusinessRetail",
  "LicenseState": "Licensed",
  "EntitlementStatus": "Provisioned",
  "TenantId": "<GUID-TENANT>",
  "LicenseId": "EWW_<ID-ANONYMISE>"
}

Vérifiez également la présence des fichiers, sans afficher leur contenu :

PowerShell
Get-ChildItem "$env:LOCALAPPDATA\Microsoft\Office\Licenses" `
  -Recurse -Force -File -ErrorAction SilentlyContinue |
  Select-Object FullName,Length,CreationTime,LastWriteTime

La documentation Microsoft explique les valeurs de LicensingNext, le dossier Licenses et l’usage de vNextDiag.ps1 : Vérifier l’état de licence et d’activation de Microsoft 365 Apps.

3. Vérifier l’état de jointure, sans conclure trop vite

PowerShell
dsregcmd.exe /status

Une activation utilisateur de Microsoft 365 Apps n’exige pas, à elle seule, que le poste soit joint à Entra ID. Dans ce cas, l’activation a réussi alors que les valeurs suivantes sont restées inchangées :

Sortie
DomainJoined    : YES
AzureAdJoined   : NO
WorkplaceJoined : NO
AzureAdPrt      : NO
WamDefaultSet   : NO

L’absence de jointure hybride était donc un contexte, pas une cause.

4. Vérifier le TPM sans exposer son secret propriétaire

N’utilisez pas Get-Tpm | Format-List * dans un journal partageable : cela peut écrire OwnerAuth, qui est une donnée sensible.

PowerShell
Get-Tpm | Select-Object `
  TpmPresent,TpmReady,TpmEnabled,TpmActivated,TpmOwned,
  RestartPending,ManufacturerIdTxt,ManufacturerVersionFull20,
  ManagedAuthLevel,OwnerClearDisabled,AutoProvisioning,
  LockedOut,LockoutCount |
  Format-List

Le TPM de ce poste était présent, prêt, activé, provisionné et non verrouillé. Les événements TPM-WMI confirmaient le bon fonctionnement du matériel et de l’attestation.

Le code 0x8029040E a une signification précise dans Winerror : TPM_E_PCP_WRONG_PARENT, c’est-à-dire qu’une clé a été chargée sous un mauvais parent de stockage. Voir COM Error Codes — TPM.

Cette distinction est essentielle : un état TPM sain n’empêche pas une clé applicative locale d’être orpheline ou incohérente.

5. Lire le journal AAD et conserver l’erreur interne

PowerShell
$since = (Get-Date).AddHours(-24)
$events = Get-WinEvent -FilterHashtable @{
  LogName   = 'Microsoft-Windows-AAD/Operational'
  StartTime = $since
} -ErrorAction SilentlyContinue

$events |
  Select-Object TimeCreated,Id,LevelDisplayName,Message |
  Format-List

"WrongParent=" + @($events | Where-Object Message -match `
  '0x8029040E|TPM_E_PCP_WRONG_PARENT').Count

Il ne faut pas s’arrêter au message supérieur Device information is missing. Dans ce cas, la chaîne d’appel donnait la vraie piste :

Sortie
GetTokenSilently
NgcHelper::SignWithSymmetricPopKey
0x8029040E TPM_E_PCP_WRONG_PARENT

6. Contrôler l’heure avec une source Internet hors site

L’absence de contrôleur de domaine était normale puisque le poste était hors site. La pile de temps a été vérifiée contre une source externe :

PowerShell
w32tm.exe /query /status /verbose
w32tm.exe /query /peers

Une dérive importante peut casser OAuth, mais l’heure était correcte après synchronisation et les erreurs TPM ont persisté. L’horloge n’était donc pas la cause.

Ce qui avait déjà été écarté

Avant d’agir sur WAM, les points suivants avaient été vérifiés :

  • licence présente et plans actifs dans le tenant ;
  • réparation en ligne d’Office sans effet ;
  • DISM /RestoreHealth réussi ;
  • sfc /scannow sans violation ;
  • absence de stratégies Office HKLM/HKCU ;
  • caches du compte Microsoft personnel retirés ;
  • cache produit reconstruit de O365HomePremRetail vers O365BusinessRetail ;
  • credentials Windows, OneAuth et comptes WAM déjà purgés puis reconnectés ;
  • aucune automatisation Outlook responsable des lancements ;
  • aucun signe de panne du TPM matériel.

Chronologie des tentatives et résultat de chacune

ÉtapeActionRésultat
1Diagnostic initial completEnviron 700 erreurs AAD/24 h, aucune licence vNext
2Synchronisation NTP InternetHeure correcte, erreur inchangée
3Purges ciblées déjà sauvegardées : Office Licenses, OneAuth, IdentityCache, comptes WAMProduit correct reconstruit, activation toujours impossible
4Réenregistrement AppX du Broker AAD et de Cloud Experience HostPackage Status=Ok, mais 3 nouveaux 0x8029040E dès OneDrive
5ProtectionPolicy=1 après export de la clé, puis redémarrage3 nouveaux 0x8029040E : insuffisant seul
6Inventaire TPM/NGC/DPAPITPM sain ; erreur limitée à l’état utilisateur WAM
7Sauvegarde intégrale et vérifiée du package AAD Broker56 fichiers source, 56 fichiers sauvegardés et hachés
8Reset-AppxPackage du seul Microsoft.AAD.BrokerPluginWrongParentCount=0 après relance OneDrive
9Connexion manuelle dans Word au vrai compte professionnelLicence O365BusinessRetail acquise, Licensed
10Ouverture d’Outlook classiqueFenêtre blanche plusieurs minutes, puis boîte chargée et connectée

Pourquoi les premières réparations n’ont pas suffi

Réenregistrer le Broker n’efface pas son état utilisateur

La commande suivante réenregistre le manifeste, mais conserve les données locales du package :

PowerShell
$pkg = Get-AppxPackage -Name Microsoft.AAD.BrokerPlugin
Add-AppxPackage -Register "$($pkg.InstallLocation)\AppxManifest.xml" `
  -DisableDevelopmentMode -ForceApplicationShutdown

Le package était déjà déclaré Ok. Le réenregistrement n’a donc pas remplacé la clé protégée devenue incohérente. Le test OneDrive réalisé juste après a reproduit trois erreurs TPM_E_PCP_WRONG_PARENT.

ProtectionPolicy=1 n’était pas la solution isolée

Microsoft documente ProtectionPolicy=1 parmi les pistes pour les erreurs d’activation liées au TPM : Microsoft 365 Apps activation error: “Trusted Platform Module malfunctioned”.

Dans ce cas, la valeur a été exportée puis ajoutée, suivie d’un redémarrage. L’erreur a persisté. Ce test évite d’attribuer à tort la résolution à la seule valeur de registre.

La stratégie a néanmoins été conservée pendant la recréation du Broker, conformément à la séquence de dépannage Microsoft. Elle doit être documentée et réévaluée selon la politique DPAPI de l’entreprise.

Effacer le TPM aurait été disproportionné

Le volume système était chiffré mais la protection BitLocker apparaissait suspendue et les protecteurs n’étaient pas clairement remontés. Un effacement TPM aurait pu affecter BitLocker, Windows Hello et d’autres clés. Comme le TPM matériel était sain et que l’erreur concernait un état WAM utilisateur, il n’a pas été effacé.

Correction qui a fonctionné

1. Fermer Office et arrêter proprement OneDrive

Le script fourni plus bas refuse la réparation si une application Office est ouverte. Il arrête OneDrive avec /shutdown au lieu de tuer indistinctement ses processus.

2. Sauvegarder le package et les clés de registre

Le dossier suivant a été copié intégralement :

Sortie
%LOCALAPPDATA%\Packages\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy

La copie a été validée par :

  • comparaison du nombre de fichiers ;
  • comparaison du nombre d’octets ;
  • calcul SHA-256 de chaque fichier ;
  • export des clés AAD, WorkplaceJoin, Office Identity, Office Licensing et AppModel concernées ;
  • ACL limitée à l’utilisateur, SYSTEM et Administrateurs.

3. Réinitialiser uniquement le package utilisateur AAD Broker

PowerShell
Get-AppxPackage -Name Microsoft.AAD.BrokerPlugin | Reset-AppxPackage

Cette commande réinitialise les données du package pour l’utilisateur courant. Elle doit être exécutée dans le profil affecté. Dans ce dossier réel, les 56 fichiers avaient été sauvegardés avant l’opération.

Microsoft recommande également la suppression ciblée des données BrokerPlugin dans son guide TPM. Ici, Reset-AppxPackage a fourni une limite plus claire et un résultat immédiatement mesurable. Des cas Microsoft Q&A décrivent la même piste pour un profil WAM corrompu : SSO Failure: Microsoft Apps for single user on VDI.

4. Relancer OneDrive comme test WAM

OneDrive constituait un excellent déclencheur reproductible de GetTokenSilently.

Résultat avant reset :

Sortie
AAD events     : 5
WrongParent    : 3
OneDrive       : lancé

Résultat après reset :

Sortie
AAD events     : 9
WrongParent    : 0
OneDrive       : lancé et stable

Les neuf événements restants étaient des avertissements d’initialisation ou liés à l’absence normale de DC/fédération, pas 0x8029040E.

5. Ouvrir Word et se connecter manuellement

Le script ne saisit jamais d’identifiants. L’utilisateur ouvre Word et se connecte dans la fenêtre Microsoft avec son vrai compte Microsoft 365 professionnel.

Si Windows propose d’inscrire ou d’administrer l’appareil alors que ce n’est pas souhaité, choisir « Non, se connecter à cette application uniquement ». Ne transformez pas un dépannage de licence en inscription Entra ou MDM non planifiée.

Validation de la réussite

Une résolution n’est pas prouvée par la seule disparition de la fenêtre d’erreur. Les contrôles suivants ont été obtenus :

Sortie
Product               : O365BusinessRetail
Activation mode       : vNext
License type          : User|Subscription
LicenseState          : Licensed
EntitlementStatus     : Provisioned
License files         : 1
TPM_E_PCP_WRONG_PARENT: 0 après correction
0xCAA5004B             : 0 après correction
Word                   : activé
OneDrive               : connecté
Outlook classique      : connecté et réactif

Le premier démarrage d’Outlook a affiché une grande fenêtre blanche pendant plusieurs minutes. Le processus n’était pas déclaré bloqué, consommait encore du CPU et maintenait plusieurs connexions TLS vers Microsoft 365. Il a finalement chargé la boîte. Cette phase doit être observée avant de tuer Outlook ou de reconstruire inutilement le profil MAPI.

Réponses aux questions fréquentes

Que signifie O365BusinessRetail.TenantId = "1," ?

Cette valeur Click-to-Run n’est ni un GUID de tenant valide ni une preuve fiable de la licence vNext. Microsoft ne publie pas de sémantique permettant de l’interpréter comme un compteur. Dans ce cas, "1," est resté présent après l’activation réussie, tandis que vNextDiag.ps1 et le fichier de licence affichaient le vrai GUID du tenant.

Conclusion opérationnelle : considérez cette valeur comme une métadonnée C2R interne et non autoritative. Ne la modifiez pas manuellement. Pour connaître le tenant réellement associé à la licence, utilisez vNextDiag.ps1.

DeployedInAuditMode = True bloque-t-il l’activation ?

Non dans ce cas. La valeur est restée à True avant et après l’acquisition réussie de la licence. La basculer arbitrairement à False aurait modifié un état de déploiement Click-to-Run sans preuve qu’il intervenait dans WAM.

Conclusion : ne changez pas cette valeur comme « recette » d’activation. Son rôle exact n’est pas documenté publiquement de façon suffisante pour justifier une modification isolée.

« Device information is missing » est-il normal sans jointure Entra ?

L’absence de données de périphérique peut apparaître dans les chemins WAM d’un poste sans PRT ni jointure Entra. Le texte supérieur n’est pas suffisant pour conclure.

Dans ce cas, l’élément bloquant était l’erreur interne TPM_E_PCP_WRONG_PARENT. Après correction, Office s’est activé avec AzureAdJoined = NO, WorkplaceJoined = NO et AzureAdPrt = NO. Une jointure Entra n’était donc pas requise pour cette licence utilisateur.

Comment forcer une nouvelle acquisition de licence sans réinstaller Office ?

Pour inspecter et retirer une licence vNext existante de manière ciblée :

PowerShell
& 'C:\Program Files\Microsoft Office\root\Office16\vNextDiag.ps1' -action list

# Après sauvegarde, utiliser uniquement le LicenseId EWW affiché :
& 'C:\Program Files\Microsoft Office\root\Office16\vNextDiag.ps1' `
  -action remove -LicenseId 'EWW_<ID>'

Au prochain lancement, Word redemande une activation. L’authentification doit rester interactive ; ne placez jamais un mot de passe, un jeton ou un code MFA dans un script.

Dans le cas décrit, retirer la licence ne suffisait pas, car WAM ne pouvait pas signer la demande. Il fallait d’abord réparer le Broker AAD.

Comment distinguer un problème profil d’un problème machine sans utiliser un autre compte ?

Plusieurs indices concordants sont utiles :

  1. Le TPM machine est sain (Get-Tpm, TPM-WMI, attestation).
  2. Le package système est installé et Status=Ok.
  3. L’échec se produit dans %LOCALAPPDATA% et HKCU.
  4. La sauvegarde puis la réinitialisation du package utilisateur suppriment l’erreur sans modifier le TPM ni la jointure machine.
  5. Un test A/B avec OneDrive donne WrongParent > 0 avant et WrongParent = 0 après.

Ce test réversible du Broker constitue ici une discrimination profil/machine forte, sans ouvrir une session avec un deuxième utilisateur.

Était-ce lié aux incidents Microsoft 365 d’août-septembre 2026 ?

Deux incidents étaient temporellement proches :

  • OP1460815, le 24 août 2026, avec des invites répétées d’activation Office rapportées par la communauté ;
  • MO1465074, commencé le 31 août 2026, lié à une configuration d’authentification centrale et affectant plusieurs services Microsoft 365.

Des administrateurs ont effectivement signalé des problèmes généralisés sur Reddit à propos de MO1465074, et une chronologie publique de l’incident est conservée par PennO365.

Cependant, les mesures locales ne permettent pas d’attribuer ce cas à la panne Microsoft :

  • les autres postes du parc s’activaient ;
  • les incidents étaient déclarés résolus ;
  • l’erreur exacte 0x8029040E est locale et signifie « mauvais parent de stockage » ;
  • le problème était reproductible à chaque appel WAM ;
  • la réinitialisation locale du Broker l’a supprimé immédiatement.

Hypothèse prudente : l’incident cloud ou une demande de renouvellement a pu révéler un cache local déjà incohérent. Aucune preuve ne permet d’affirmer qu’il l’a créé.

Ne pas confondre les erreurs apparues après la correction

Après réparation, deux appels ont produit AADSTS500014 vers un ancien tenant SharePoint différent du tenant de licence. L’erreur interne précisait que le principal de service de la ressource SharePoint y était désactivé.

Ce signal était distinct :

  • autre tenant dans l’authority OAuth ;
  • aucun 0x8029040E ;
  • aucun 0xCAA5004B ;
  • licence toujours Licensed ;
  • Outlook et OneDrive connectés.

Cela illustre une règle de diagnostic importante : deux événements AAD 1098 ne représentent pas nécessairement la même panne. Lisez le code interne, l’authority, le client et la ressource.

Arbre de décision recommandé

  1. Vérifier la licence et les plans dans le tenant.
  2. Vérifier l’état de service Microsoft 365 si plusieurs utilisateurs sont touchés.
  3. Contrôler l’heure, DNS et HTTPS.
  4. Lire vNextDiag.ps1 et le dossier Office\Licenses.
  5. Lire dsregcmd /status, sans exiger une jointure Entra non prévue.
  6. Lire AAD/Operational et rechercher l’erreur interne.
  7. Si 0x8029040E est présent, vérifier le TPM sans l’effacer.
  8. Réenregistrer le Broker AAD : test non destructif.
  9. Si l’erreur persiste, sauvegarder intégralement le package utilisateur puis le réinitialiser.
  10. Reconnecter manuellement Word et vérifier Licensed.
  11. Tester OneDrive et Outlook, puis vérifier l’absence de récidive.
  12. N’envisager l’effacement TPM qu’en dernier recours, avec protecteurs BitLocker et clés de récupération vérifiés.

Sources techniques

Une annexe de preuves anonymisées reprend les sorties déterminantes, y compris les tentatives infructueuses et la validation finale. Les journaux bruts ne doivent pas être publiés.

Script PowerShell complet : diagnostic et dry-run

Le script ci-dessous est compatible Windows PowerShell 5.1.

Comportement de sécurité :

  • Diagnose est l’action par défaut et reste en lecture seule ;
  • toute action de réparation est simulée tant que -Apply n’est pas fourni ;
  • ResetBroker effectue et vérifie une sauvegarde avant la réinitialisation ;
  • le script refuse -Apply si une application Office est ouverte ;
  • aucun identifiant, mot de passe ou jeton MFA n’est demandé ;
  • OwnerAuth du TPM n’est jamais journalisé ;
  • le dossier de traces est protégé par ACL ;
  • aucun redémarrage n’est forcé.

Exemples :

PowerShell
# Diagnostic en lecture seule
.\Office-WAM-Activation-Debug.ps1

# Voir exactement ce que ferait la réparation, sans la faire
.\Office-WAM-Activation-Debug.ps1 -Action ResetBroker

# Réenregistrement non destructif du Broker
.\Office-WAM-Activation-Debug.ps1 -Action ReregisterBroker -Apply

# Reset du Broker après sauvegarde intégrale vérifiée
.\Office-WAM-Activation-Debug.ps1 -Action ResetBroker -Apply

# Simulation de la stratégie DPAPI documentée par Microsoft
.\Office-WAM-Activation-Debug.ps1 -Action SetProtectionPolicy

# Application réelle : lancer PowerShell en administrateur
.\Office-WAM-Activation-Debug.ps1 -Action SetProtectionPolicy -Apply
Les journaux originaux peuvent contenir des SID, GUID de tenant, identifiants de licence et chemins de profil. Anonymisez-les avant publication.
PowerShell
[CmdletBinding()]
param(
    [ValidateSet('Diagnose','ReregisterBroker','ResetBroker','SetProtectionPolicy','RestoreProtectionPolicy')]
    [string]$Action = 'Diagnose',
    [switch]$Apply,
    [ValidateRange(1,168)]
    [int]$EventHours = 24,
    [string]$OutputRoot = 'C:\INFORMATIQUE',
    [string]$RestoreFrom
)

# PowerShell 5.1 compatible. No credential is requested, read or stored.
# Diagnose is always read-only. Every repair action is a dry run unless -Apply is present.

$ErrorActionPreference = 'Stop'
$script:DryRun = -not $Apply
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$machineSafe = $env:COMPUTERNAME -replace '[^A-Za-z0-9_-]', '_'
$runRoot = Join-Path $OutputRoot "Office-WAM-Debug-$machineSafe-$stamp"
$logRoot = Join-Path $runRoot 'Logs'
$backupRoot = Join-Path $runRoot 'Backup'
New-Item -ItemType Directory -Path $logRoot,$backupRoot -Force | Out-Null

# Diagnostic files can contain technical identifiers. Restrict access immediately.
$userSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
& icacls.exe $runRoot /inheritance:r /grant:r "*$userSid`:(OI)(CI)F" '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'Unable to secure the diagnostic directory ACL.' }

$script:TranscriptPath = Join-Path $logRoot 'Transcript.txt'
Start-Transcript -LiteralPath $script:TranscriptPath -Force | Out-Null

function Write-Log {
    param([string]$Message)
    $line = '[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'),$Message
    Write-Host $line
    $line | Add-Content -LiteralPath (Join-Path $logRoot 'Actions.log') -Encoding UTF8
}

function Save-Output {
    param([string]$Name,[scriptblock]$Command)
    try {
        & $Command 2>&1 | Out-File -LiteralPath (Join-Path $logRoot "$Name.txt") -Encoding UTF8 -Width 400
    }
    catch {
        $_ | Format-List * -Force | Out-File -LiteralPath (Join-Path $logRoot "$Name-ERROR.txt") -Encoding UTF8 -Width 400
    }
}

function Test-IsAdministrator {
    $p = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
    $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-Office16Path {
    @(
        'C:\Program Files\Microsoft Office\root\Office16',
        'C:\Program Files (x86)\Microsoft Office\root\Office16',
        'C:\Program Files\Microsoft Office\Office16',
        'C:\Program Files (x86)\Microsoft Office\Office16'
    ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
}

function Get-TreeStats {
    param([Parameter(Mandatory=$true)][string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) {
        return [pscustomobject]@{Path=$Path;Exists=$false;Files=0;Bytes=0}
    }
    $m = Get-ChildItem -LiteralPath $Path -Recurse -Force -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum
    [pscustomobject]@{Path=$Path;Exists=$true;Files=[int]$m.Count;Bytes=[long]$m.Sum}
}

function Export-RegIfPresent {
    param([string]$NativeKey,[string]$FileName)
    & reg.exe query $NativeKey *> $null
    if ($LASTEXITCODE -eq 0) {
        $destination = Join-Path $backupRoot $FileName
        & reg.exe export $NativeKey $destination /y | Out-Null
        if ($LASTEXITCODE -ne 0) { throw "Registry backup failed: $NativeKey" }
        Write-Log "Registry backup: $NativeKey"
    }
}

function Backup-TreeVerified {
    param([string]$Source,[string]$Destination,[string]$Label)
    if (-not (Test-Path -LiteralPath $Source)) {
        Write-Log "Backup source absent: $Source"
        return
    }
    $before = Get-TreeStats $Source
    New-Item -ItemType Directory -Path $Destination -Force | Out-Null
    & robocopy.exe $Source $Destination /E /COPY:DAT /DCOPY:DAT /R:1 /W:1 /XJ /NP /NFL /NDL /NJH /NJS |
        Out-File -LiteralPath (Join-Path $logRoot "Robocopy-$Label.txt") -Encoding UTF8
    if ($LASTEXITCODE -ge 8) { throw "Robocopy failed for $Label with exit code $LASTEXITCODE" }
    $after = Get-TreeStats $Destination
    @($before,$after) | Export-Csv -LiteralPath (Join-Path $logRoot "Backup-Verification-$Label.csv") -NoTypeInformation -Encoding UTF8
    if ($after.Files -lt $before.Files -or $after.Bytes -lt $before.Bytes) {
        throw "Incomplete backup for $Label"
    }
    Get-ChildItem -LiteralPath $Destination -Recurse -Force -File -ErrorAction Stop | Get-FileHash -Algorithm SHA256 |
        Export-Csv -LiteralPath (Join-Path $logRoot "Backup-SHA256-$Label.csv") -NoTypeInformation -Encoding UTF8
    Write-Log "Verified backup: $Label ($($after.Files) files, $($after.Bytes) bytes)"
}

function Assert-OfficeClosed {
    $names = @('WINWORD','OUTLOOK','EXCEL','POWERPNT','MSACCESS','MSPUB','ONENOTE','LYNC')
    $running = @(Get-Process -Name $names -ErrorAction SilentlyContinue)
    if ($running) {
        throw ('Close Office applications before -Apply. Running: ' + (($running.ProcessName | Sort-Object -Unique) -join ', '))
    }
}

function Stop-OneDriveCleanly {
    $exe = @(
        (Join-Path $env:LOCALAPPDATA 'Microsoft\OneDrive\OneDrive.exe'),
        'C:\Program Files\Microsoft OneDrive\OneDrive.exe',
        'C:\Program Files (x86)\Microsoft OneDrive\OneDrive.exe'
    ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
    if ($exe -and (Get-Process OneDrive -ErrorAction SilentlyContinue)) {
        Start-Process -FilePath $exe -ArgumentList '/shutdown' -Wait
        Start-Sleep -Seconds 4
        Write-Log 'OneDrive stopped cleanly.'
    }
}

function Write-Diagnostics {
    param([string]$Prefix)
    $since = (Get-Date).AddHours(-$EventHours)
    $office16 = Get-Office16Path
    $vNextDiag = if ($office16) { Join-Path $office16 'vNextDiag.ps1' } else { $null }
    $brokerData = Join-Path $env:LOCALAPPDATA 'Packages\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy'

    Save-Output "$Prefix-00-Context" {
        [pscustomobject]@{
            Captured=(Get-Date -Format o)
            Computer=$env:COMPUTERNAME
            User=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
            UserSid=$userSid
            Elevated=(Test-IsAdministrator)
            Windows=(Get-CimInstance Win32_OperatingSystem).Caption
            WindowsVersion=(Get-CimInstance Win32_OperatingSystem).Version
            PowerShell=$PSVersionTable.PSVersion.ToString()
            Action=$Action
            DryRun=$script:DryRun
        } | Format-List
    }
    Save-Output "$Prefix-01-Office-ClickToRun" {
        Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -ErrorAction SilentlyContinue |
            Select-Object ProductReleaseIds,VersionToReport,ClientVersionToReport,UpdateChannel,CDNBaseUrl,Platform,'O365BusinessRetail.TenantId','O365BusinessRetail.OSPPReady',DeployedInAuditMode | Format-List
    }
    Save-Output "$Prefix-02-Office-User-Licensing" {
        Get-ItemProperty -LiteralPath 'HKCU:\SOFTWARE\Microsoft\Office\16.0\Common\Licensing' -ErrorAction SilentlyContinue |
            Select-Object TimeOfLastDACheck,DeviceEntitlementsExist,EligibleForExtendedGrace | Format-List
        Get-ItemProperty -LiteralPath 'HKCU:\SOFTWARE\Microsoft\Office\16.0\Common\Licensing\LicensingNext' -ErrorAction SilentlyContinue | Format-List
        Get-TreeStats (Join-Path $env:LOCALAPPDATA 'Microsoft\Office\Licenses') | Format-List
    }
    Save-Output "$Prefix-03-vNextDiag" {
        if ($vNextDiag -and (Test-Path -LiteralPath $vNextDiag)) { & $vNextDiag -action list }
        else { 'vNextDiag.ps1 not found.' }
    }
    Save-Output "$Prefix-04-DsRegCmd" { dsregcmd.exe /status }
    Save-Output "$Prefix-05-TPM" {
        # Never use Format-List * here: OwnerAuth is a TPM secret.
        Get-Tpm | Select-Object TpmPresent,TpmReady,TpmEnabled,TpmActivated,TpmOwned,RestartPending,ManufacturerIdTxt,ManufacturerVersionFull20,ManagedAuthLevel,OwnerClearDisabled,AutoProvisioning,LockedOut,LockoutCount | Format-List
    }
    Save-Output "$Prefix-06-Time" {
        w32tm.exe /query /status /verbose
        w32tm.exe /query /peers
    }
    Save-Output "$Prefix-07-Appx-Broker" {
        Get-AppxPackage -Name Microsoft.AAD.BrokerPlugin | Select-Object Name,PackageFullName,Status,InstallLocation | Format-List
        Get-AppxPackage -Name Microsoft.Windows.CloudExperienceHost | Select-Object Name,PackageFullName,Status,InstallLocation | Format-List
        Get-TreeStats $brokerData | Format-List
    }
    Save-Output "$Prefix-08-Processes-Services" {
        Get-Process WINWORD,OUTLOOK,EXCEL,POWERPNT,ONENOTE,OneDrive,Microsoft.AAD.BrokerPlugin -ErrorAction SilentlyContinue |
            Select-Object ProcessName,Id,StartTime,Responding,Path | Format-Table -AutoSize
        Get-Service ClickToRunSvc,TokenBroker -ErrorAction SilentlyContinue | Select-Object Name,Status,StartType | Format-Table -AutoSize
    }
    Save-Output "$Prefix-09-Network" {
        'login.microsoftonline.com','officeclient.microsoft.com','ols.officeapps.live.com' | ForEach-Object {
            Test-NetConnection -ComputerName $_ -Port 443 -InformationLevel Detailed
        }
    }
    Save-Output "$Prefix-10-AAD-Events" {
        Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-AAD/Operational';StartTime=$since} -ErrorAction SilentlyContinue |
            Select-Object TimeCreated,Id,LevelDisplayName,ProviderName,Message | Format-List
    }
    $events = @(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-AAD/Operational';StartTime=$since} -ErrorAction SilentlyContinue)
    @(
        "Captured=$(Get-Date -Format o)"
        "WindowHours=$EventHours"
        "AAD_EventCount=$($events.Count)"
        "AAD_ErrorCount=$(@($events | Where-Object Level -eq 2).Count)"
        "WrongParentCount=$(@($events | Where-Object Message -match '0x8029040E|TPM_E_PCP_WRONG_PARENT').Count)"
        "CAA5004BCount=$(@($events | Where-Object Message -match '0xCAA5004B').Count)"
        "CAA5001CCount=$(@($events | Where-Object Message -match '0xCAA5001C').Count)"
    ) | Out-File -LiteralPath (Join-Path $logRoot "$Prefix-11-Summary.txt") -Encoding UTF8
}

function Backup-BrokerState {
    $packageFamily = 'Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy'
    Export-RegIfPresent 'HKCU\Software\Microsoft\Windows\CurrentVersion\AAD' 'HKCU-AAD.reg'
    Export-RegIfPresent 'HKCU\Software\Microsoft\Windows NT\CurrentVersion\WorkplaceJoin' 'HKCU-WorkplaceJoin.reg'
    Export-RegIfPresent 'HKCU\Software\Microsoft\Office\16.0\Common\Identity' 'HKCU-Office-Identity.reg'
    Export-RegIfPresent 'HKCU\Software\Microsoft\Office\16.0\Common\Licensing' 'HKCU-Office-Licensing.reg'
    Export-RegIfPresent 'HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\SystemAppData\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy' 'HKCU-AadBroker-SystemAppData.reg'
    Backup-TreeVerified (Join-Path $env:LOCALAPPDATA "Packages\$packageFamily") (Join-Path $backupRoot $packageFamily) 'AadBroker'
}

try {
    Write-Log "Action=$Action Apply=$Apply DryRun=$script:DryRun"
    Write-Diagnostics 'Before'

    switch ($Action) {
        'Diagnose' {
            Write-Log 'Read-only diagnosis completed. No repair was attempted.'
        }
        'ReregisterBroker' {
            if ($script:DryRun) {
                Write-Log 'DRY RUN: would close broker-hosted apps and re-register Microsoft.AAD.BrokerPlugin.'
            }
            else {
                Assert-OfficeClosed
                Stop-OneDriveCleanly
                $pkg = Get-AppxPackage -Name Microsoft.AAD.BrokerPlugin
                if (-not $pkg) { throw 'Microsoft.AAD.BrokerPlugin package not found.' }
                Add-AppxPackage -Register (Join-Path $pkg.InstallLocation 'AppxManifest.xml') -DisableDevelopmentMode -ForceApplicationShutdown
                Write-Log 'AAD BrokerPlugin re-registered.'
                Write-Diagnostics 'After'
            }
        }
        'ResetBroker' {
            $source = Join-Path $env:LOCALAPPDATA 'Packages\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy'
            $stats = Get-TreeStats $source
            Write-Log "Broker source: Exists=$($stats.Exists), Files=$($stats.Files), Bytes=$($stats.Bytes)"
            if ($script:DryRun) {
                Write-Log 'DRY RUN: would export relevant registry keys, copy and hash the complete BrokerPlugin data tree, verify the backup, stop OneDrive, then run Reset-AppxPackage.'
            }
            else {
                Assert-OfficeClosed
                Backup-BrokerState
                Stop-OneDriveCleanly
                $pkg = Get-AppxPackage -Name Microsoft.AAD.BrokerPlugin
                if (-not $pkg) { throw 'Microsoft.AAD.BrokerPlugin package not found.' }
                $pkg | Reset-AppxPackage
                Write-Log 'AAD BrokerPlugin user data reset after verified backup.'
                Write-Diagnostics 'After'
                Write-Log 'Open Word manually and sign in through the Microsoft window. Never put credentials in this script.'
            }
        }
        'SetProtectionPolicy' {
            if (-not $script:DryRun -and -not (Test-IsAdministrator)) { throw 'SetProtectionPolicy with -Apply requires an elevated PowerShell window.' }
            $native = 'HKLM\SOFTWARE\Microsoft\Cryptography\Protect\Providers\df9d8cd0-1501-11d1-8c7a-00c04fc297eb'
            $psPath = 'HKLM:\SOFTWARE\Microsoft\Cryptography\Protect\Providers\df9d8cd0-1501-11d1-8c7a-00c04fc297eb'
            $item = Get-ItemProperty -LiteralPath $psPath -ErrorAction SilentlyContinue
            $exists = $null -ne $item -and $item.PSObject.Properties.Name -contains 'ProtectionPolicy'
            $old = if ($exists) { [int]$item.ProtectionPolicy } else { $null }
            [pscustomobject]@{Exists=$exists;Value=$old;RegistryPath=$psPath} | Export-Clixml -LiteralPath (Join-Path $backupRoot 'ProtectionPolicy-Before.clixml')
            Export-RegIfPresent $native 'HKLM-DPAPI-Provider.reg'
            if ($script:DryRun) {
                Write-Log "DRY RUN: would set ProtectionPolicy=1. PreviousExists=$exists PreviousValue=$old"
            }
            else {
                New-Item -Path $psPath -Force | Out-Null
                New-ItemProperty -LiteralPath $psPath -Name ProtectionPolicy -PropertyType DWord -Value 1 -Force | Out-Null
                Write-Log 'ProtectionPolicy set to 1. Reboot is required for a meaningful test.'
                Write-Diagnostics 'After'
            }
        }
        'RestoreProtectionPolicy' {
            if (-not $script:DryRun -and -not (Test-IsAdministrator)) { throw 'RestoreProtectionPolicy with -Apply requires an elevated PowerShell window.' }
            if (-not $RestoreFrom) { throw 'Use -RestoreFrom with the earlier Office-WAM-Debug run directory.' }
            $resolved = (Resolve-Path -LiteralPath $RestoreFrom).Path
            if (-not $resolved.StartsWith((Join-Path $OutputRoot 'Office-WAM-Debug-'),[System.StringComparison]::OrdinalIgnoreCase)) {
                throw "Restore path refused: $resolved"
            }
            $stateFile = Join-Path $resolved 'Backup\ProtectionPolicy-Before.clixml'
            $state = Import-Clixml -LiteralPath $stateFile
            if ($script:DryRun) {
                Write-Log "DRY RUN: would restore ProtectionPolicy. PreviousExists=$($state.Exists) PreviousValue=$($state.Value)"
            }
            else {
                if ($state.Exists) {
                    New-ItemProperty -LiteralPath $state.RegistryPath -Name ProtectionPolicy -PropertyType DWord -Value ([int]$state.Value) -Force | Out-Null
                }
                else {
                    Remove-ItemProperty -LiteralPath $state.RegistryPath -Name ProtectionPolicy -ErrorAction SilentlyContinue
                }
                Write-Log 'ProtectionPolicy restored to its recorded previous state.'
                Write-Diagnostics 'After'
            }
        }
    }

    @(
        "RunRoot=$runRoot"
        "Action=$Action"
        "DryRun=$script:DryRun"
        "Completed=$(Get-Date -Format o)"
        'CredentialsRequested=False'
    ) | Out-File -LiteralPath (Join-Path $runRoot 'RESULT.txt') -Encoding UTF8

    Stop-Transcript | Out-Null
    Get-ChildItem -LiteralPath $runRoot -Recurse -Force -File -ErrorAction SilentlyContinue |
        Where-Object FullName -ne (Join-Path $runRoot 'SHA256.csv') |
        Get-FileHash -Algorithm SHA256 -ErrorAction SilentlyContinue |
        Export-Csv -LiteralPath (Join-Path $runRoot 'SHA256.csv') -NoTypeInformation -Encoding UTF8

    Write-Host "Logs: $runRoot"
    exit 0
}
catch {
    $_ | Format-List * -Force | Out-File -LiteralPath (Join-Path $runRoot 'ERROR.txt') -Encoding UTF8 -Width 400
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    Write-Error $_
    Write-Host "Logs: $runRoot"
    exit 1
}

Télécharger le script PowerShell

Le fichier téléchargeable est strictement identique au script complet affiché ci-dessus.

Télécharger Office-WAM-Activation-Debug.ps1