MSIX で配布したデスクトップ アプリをアンインストールしても TPM(Trusted Platform Module)に格納した鍵やシークレットが残る――この落とし穴は、運用に入ってから初めて気づかれることが少なくありません。結論は明快で、MSIX のアンインストールはハードウェア層のクリーンアップを行いません。本記事ではその背景、危険ポイント、実運用で使える削除戦略、C# / C++ / PowerShell の実装例、そして Intune やスクリプトを使った自動化まで、現場に持ち帰れる形で徹底解説します。
問題の背景と結論(先に要点)
結論:MSIX は「アプリ ファイルとレジストリの追加・削除」を担当する仕組みであり、TPM などのハードウェア・セキュリティ モジュールの中身には触れません。したがって、アプリをアンインストールしても TPM 内に永続化した鍵/シークレットは自動では消えません。必要ならばアプリ側の明示的な削除機構、または外部スクリプトや運用フローで消す設計が必須です。
| ポイント | 内容 |
|---|---|
| MSIX の仕様 | MSIX はコンテナ化されたファイル/レジストリの管理が守備範囲。TPM・スマートカード・HSM といったハードウェア層は対象外。アンインストールだけでは TPM データは消えない。 |
| 推奨対処 | 1) アプリ UI(設定 → セキュリティ → 「TPM データを削除」)を実装し、CNG/TPM API で明示削除。 2) 付属の PowerShell スクリプトを配布し、先にスクリプト → その後アンインストールの手順を周知。 3) 組織配布では Intune/構成管理で「TPM クリーン → Remove-AppxPackage」を連続実行。 |
| 自動化の工夫 | アプリ終了時のクリーンアップ、ユーザー設定に基づく自動削除、ヘッドレス モードでの削除コマンド実装など。 |
| 注意事項 | TPM への書き込み/削除は状況により管理者権限が必要になる。Clear-Tpm はマシン全体の TPM を初期化するため安易に使用しない(BitLocker 等が影響)。 |
TPM と CNG(Cryptography Next Generation)基礎整理
Windows では、CNG(CNG API / NCrypt* ファミリ)を介して TPM を含む各種 Key Storage Provider(KSP)を扱います。アプリが TPM に鍵を「永続化(persist)」した場合、その鍵は OS のアンインストーラーからは独立しており、MSIX の削除処理では影響を受けません。
| 対象 | 代表例 | 削除の可否 | 備考 |
|---|---|---|---|
| アプリ ファイル/仮想化レジストリ | 実行ファイル、設定ファイル、MSIX 仮想レジストリ | MSIX アンインストールで削除 | MSIX の守備範囲 |
| TPM 永続鍵 | Microsoft Platform Crypto Provider に保管された鍵 | アプリ側で削除が必要 | NCrypt API / CNG を使用 |
| TPM を使って封緘されたデータ | TPM バインドされたシークレット(復号は当該 TPM のみ) | アプリ側で削除が必要 | 封緘データ(バイト列)自体の廃棄が必要 |
| OS 全体の TPM 初期化 | tpm.msc の「TPM をクリア」、Clear-Tpm | 可能だが非推奨 | BitLocker 等のシステム機能に重大影響 |
MSIX の制約と設計指針
- アンインストール時スクリプトの同梱不可:MSIX 自体には MSI のような「アンインストール前後フック」はありません。
- 外部連携で補う:企業環境では Intune 等の配布基盤でプレ/ポスト処理を組み合わせ、TPM クリーン → パッケージ削除を連続実行するのが現実解です。
- アプリ機能で削除を可能に:UI/CLI/ヘッドレスのいずれかで「TPM データの削除」を実装。ユーザー/管理者がアンインストール前に実行できる導線を設けます。
安全な削除戦略(設計パターン集)
1) アプリ UI からの明示削除(推奨)
設定画面に「TPM データを削除」ボタンを設け、以下の順序で削除します。
- アプリが生成した鍵名(後述の命名規約)を取り出す。
- 該当鍵を CNG API でオープンし、
NCryptDeleteKey(C++)またはCngKey.Delete()(C#)で削除。 - TPM に封緘保存していたシークレット バイト列(ファイル/DB)をゼロ化してから削除。
- 削除結果を検証(存在確認 or 再オープンに失敗することを確認)。
2) アプリ終了時のクリーンアップ
ユーザーが UI 操作を忘れても、アプリ終了フックで自動削除できます。「次回起動後に自動削除」オプションを用意し、運用都合に合わせて切替可能にするのが実務的です。
3) 自動化(組織配布向け)
- Intune / 管理スクリプト:PowerShell で鍵削除 →
Remove-AppxPackageを順次実行。 - ヘッドレス CLI:
myapp.exe --cleanup-tpmのようなサブコマンドを実装。配布基盤からサイレント実行。
4) Clear-Tpm は最終手段
TPM 全体を初期化する Clear-Tpm は、BitLocker などシステム全体に影響するためアプリのアンインストール目的では使用しないでください。どうしても必要な場合は、影響範囲の周知・バックアップ・再プロビジョニング手順を伴う正式な運用手順とし、一般ユーザーには提供しない構成が安全です。
鍵の命名規約と追跡手法(削除成功率を上げるコツ)
削除の精度は「どの鍵を自分が作ったか、確実に識別できるか」に依存します。実装初期から以下を徹底します。
- 接頭辞+GUID:
{ProductShortName}-{PFN}-{UserSid}-{GUID}など、アプリに固有の接頭辞を含む鍵名を使用。 - 鍵名の台帳化:作成時に鍵名を
%LOCALAPPDATA%\Packages\<PackageFamilyName>\LocalState\keys.json等に永続化。 - 範囲を絞った列挙:列挙 API(C++ の
NCryptEnumKeys)を使う場合も、接頭辞一致だけを対象にすることで、他システムの鍵を誤って削除しない。
C# 実装例(CNG / Microsoft Platform Crypto Provider)
以下は、台帳(keys.json)に記録した鍵名を削除する最小例です。Platform Crypto Provider を明示して開き、Delete() を呼び出します。
// 対象: .NET 6+ / Windows
// 参照: System.Security.Cryptography
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
static class TpmCleanup
{
private static readonly CngProvider Pcp = new CngProvider("Microsoft Platform Crypto Provider");
private const string LedgerPath = "keys.json"; // LocalState などに配置
public static void DeleteAllFromLedger()
{
if (!File.Exists(LedgerPath))
{
Console.WriteLine("ledger not found.");
return;
}
var names = JsonSerializer.Deserialize<List<string>>(File.ReadAllText(LedgerPath)) ?? new();
foreach (var name in names)
{
try
{
using var key = CngKey.Open(name, Pcp);
key.Delete();
Console.WriteLine($"deleted: {name}");
}
catch (CryptographicException ex)
{
Console.WriteLine($"skip {name}: {ex.Message}");
}
}
}
public static void DeleteByPrefix(string prefix)
{
// 台帳がない場合のフォールバック: 接頭辞に一致する既知の候補を生成する等、設計に応じて実装
// ここでは簡略化し、台帳経由のみとする
DeleteAllFromLedger();
}
}
ポイント:
- 台帳に鍵名を必ず記録(アプリ初回起動時/鍵作成時)。
- キーは Microsoft Platform Crypto Provider でオープンすること(
new CngProvider("Microsoft Platform Crypto Provider"))。 - 例外は握りつぶさずログ化。
NTE_BAD_KEYSET相当等の典型エラーを後述のトラブルシューティングで扱います。
C++ 実装例(NCrypt API で列挙と削除)
台帳がない/補助として列挙したい場合の例です。NCryptEnumKeys で鍵名を走査し、接頭辞一致のみを NCryptDeleteKey で削除します。
// 対象: Windows, ncrypt.h / ncrypt.lib
#include <windows.h>
#include <ncrypt.h>
#include <string>
#include <iostream>
#pragma comment(lib, "ncrypt.lib")
int wmain(int argc, wchar_t** argv)
{
const wchar_t* providerName = L"Microsoft Platform Crypto Provider";
const std::wstring prefix = L"MyApp-"; // 自アプリの接頭辞
NCRYPT_PROV_HANDLE prov = 0;
SECURITY_STATUS s = NCryptOpenStorageProvider(&prov, providerName, 0);
if (s != ERROR_SUCCESS) { std::wcerr << L"OpenStorageProvider failed: " << s << std::endl; return 1; }
PVOID enumState = nullptr;
NCryptKeyName* pKeyName = nullptr;
while ((s = NCryptEnumKeys(prov, nullptr, &pKeyName, &enumState, 0)) == ERROR_SUCCESS)
{
std::wstring name = pKeyName->pszName;
if (name.rfind(prefix, 0) == 0) // 接頭辞一致
{
NCRYPT_KEY_HANDLE key = 0;
if (NCryptOpenKey(prov, &key, name.c_str(), 0, 0) == ERROR_SUCCESS)
{
SECURITY_STATUS ds = NCryptDeleteKey(key, 0);
if (ds == ERROR_SUCCESS)
std::wcout << L"deleted: " << name << std::endl;
else
std::wcerr << L"delete failed (" << ds << L"): " << name << std::endl;
NCryptFreeObject(key);
}
}
NCryptFreeBuffer(pKeyName);
pKeyName = nullptr;
}
if (s != NTE_NO_MORE_ITEMS) // 列挙完了コード
std::wcerr << L"EnumKeys ended with: " << s << std::endl;
if (enumState) NCryptFreeBuffer(enumState);
if (prov) NCryptFreeObject(prov);
return 0;
}
ポイント:必ず接頭辞で範囲を限定し、他アプリや OS の鍵に触れないようにします。削除後に再オープンできないことを確認すると堅牢です。
PowerShell 実装例(管理者向けワンライナー/スクリプト)
PowerShell から CNG を直接叩くには P/Invoke が必要ですが、実務では certutil を併用すると簡潔です。以下は Microsoft Platform Crypto Provider にある「接頭辞一致」の鍵だけを削除するスクリプトです(管理者で実行)。
# ファイル名: cleanup-tpm-keys.ps1
param(
[Parameter(Mandatory=$true)][string]$Prefix = "MyApp-"
)
# 列挙(Key Container 名を抽出)
$lines = certutil -csp "Microsoft Platform Crypto Provider" -key 2>&1
$names = @()
foreach ($l in $lines) {
if ($l -match "Key Container = (.+)$") { $names += $matches[1] }
}
# 削除(接頭辞一致のみ)
foreach ($n in $names) {
if ($n -like "$Prefix*") {
Write-Host "Deleting $n"
certutil -csp "Microsoft Platform Crypto Provider" -delkey "$n" | Out-Null
}
}
Write-Host "done."
アプリのアンインストール手順書・README には、先にこのスクリプトを実行 → その後アンインストールと明記してください。
アンインストール自動化(Intune / 構成管理の例)
組織配布では、PowerShell を用いて TPM クリーンと MSIX の削除をまとまって実行するのが実務的です。以下は概念的な例です。
# 1) TPM クリーン(接頭辞一致)
powershell.exe -ExecutionPolicy Bypass -File .\cleanup-tpm-keys.ps1 -Prefix "MyApp-"
# 2) MSIX パッケージの削除
$pkg = Get-AppxPackage -Name "Contoso.MyApp" -AllUsers
if ($pkg) {
$pkg | Remove-AppxPackage -AllUsers
}
- 権限:
-AllUsersは管理者権限が必要です。端末管理のロールに合わせて付与。 - 検証:パッケージ削除前に TPM クリーンが完了したかログで判定し、失敗したらロールバック/再試行へ。
UI/UX 設計(ユーザーに安心して消してもらう)
- 設定画面:「セキュリティ」内にTPM データの削除(説明テキスト+確認ダイアログ)。
- 情報開示:削除対象(鍵名、作成日、用途)を一覧で確認できるようにする。
- アンインストール直前の案内:アプリからアンインストール UI を開く前に「TPM データを消しますか?」を提示。
- ヘッドレス対応:IT 管理者向けに
--cleanup-tpmオプションやcleanup.exeを提供。
権限とセキュリティ留意点
- 管理者権限:TPM の永続鍵削除は構成によっては管理者権限を要します。UAC 昇格プロンプトやサービス コンポーネントで対応。
- 誤削除の防止:接頭辞と台帳で厳密に絞り込み、他アプリ/OS 鍵は絶対に触らない。
- ログと監査:削除時刻・対象鍵名・呼出し元(UI/CLI/スクリプト)をイベント ログ等へ記録。
- Clear-Tpm の封印:通常運用から外し、実行権限を強く制限。利用時は BitLocker 等の再プロビジョニング計画が必須。
テスト計画(再現性と安全性を担保)
| 観点 | 試験内容 | 期待結果 |
|---|---|---|
| 鍵作成/台帳記録 | 初回起動で鍵生成・keys.json に記録 | 鍵名が接頭辞付きで台帳に残る |
| UI 削除 | 設定画面から削除→確認ダイアログ→実行 | 該当鍵が開けなくなり、封緘データも消える |
| 終了時削除 | 終了フックで自動削除(オプション有効) | 再起動後に鍵が存在しない |
| スクリプト実行 | 管理者で cleanup-tpm-keys.ps1 を実行 | ログに削除結果が記録、OS 鍵は無傷 |
| アンインストール連携 | TPM クリーン→Remove-AppxPackage | アプリが消え、TPM もクリーン |
| 異常系 | 鍵不存在/アクセス拒否/一部失敗 | リトライ/スキップの設計通りに振る舞う |
トラブルシューティング(典型エラーと対処)
| 症状/エラー | 原因 | 対処 |
|---|---|---|
NTE_BAD_KEYSET / 鍵が開けない | 既に削除済み/鍵名が違う | 台帳と接頭辞を見直し、存在確認を分岐 |
NTE_PERM / アクセス拒否 | 権限不足(UAC、サービス権限) | 管理者昇格、サービス経由で呼び出し |
| 他アプリの鍵を誤ってマッチ | 接頭辞の設計不備 | 固有性の高い接頭辞+PFN/SID を含める |
| 封緘データが残る | 鍵だけ削除してデータを未削除 | 鍵削除とシークレット バイト列の廃棄をセットで実装 |
Clear-Tpm で BitLocker が影響 | TPM 全体初期化の副作用 | 最終手段に限定。業務手順書と再プロビジョニング必須 |
実装チェックリスト(配布前の最終確認)
- 鍵名は接頭辞+PFN/SID/GUID で一意か。
- 鍵名の台帳(
keys.json)が作成・更新・削除の各タイミングで整合しているか。 - 削除 UI/CLI/ヘッドレスが網羅され、手順書に「TPM クリーン → アンインストール」が明記されているか。
- 管理者権限が必要なパスで UAC 対応が実装されているか。
- ログと監査(誰がいつ何を消したか)が残るか。
- Intune 等の配布基盤で自動クリーンのジョブが構成されているか。
- 異常系テスト(鍵不存在・アクセス拒否・部分失敗)に対する挙動が定義されているか。
FAQ(よくある質問)
Q. MSIX Packaging Tool に「TPM を削除する」オプションはありますか?
A. ありません。MSIX のアンインストーラーはハードウェア層を扱いません。
Q. ユーザー権限アプリでも TPM 鍵は消せますか?
A. 構成により管理者権限が必要な場合があります。UAC 昇格やバックエンド サービスで実行してください。
Q. アンインストール後に消すことはできますか?
A. 可能ですが、パッケージが消えた後は自アプリの実行が困難になるため、アンインストール前に消すフローを推奨します。やむを得ない場合は、独立した cleanup.exe を別配布し、Intune などから実行します。
Q. 鍵を消すと封緘データは自動で読み出せなくなりますか?
A. はい。ただし封緘データ(ファイル/DB のバイト列)は別途存在するため、そのものもゼロ化・削除しないとデータ残存リスクが残ります。
Q. UWP API で作った鍵もこの方針で消せますか?
A. UWP のみで作成した資材は API 面が異なりますが、パッケージ化したデスクトップ アプリ(Desktop Bridge)から CNG/NCrypt で扱うか、アプリ側の削除機構を実装するのが基本方針です。
サンプル:封緘データ(アプリ側バイト列)の安全な廃棄
TPM による鍵管理だけでなく、アプリが保持していたシークレット バイト列も安全に廃棄します。Windows ではオーバーライトを 1 回以上行い、ファイル ハンドルをフラッシュしたうえで削除するのが基本です。
// C# サンプル:安全なファイル削除(簡易版)
using System;
using System.IO;
using System.Security.Cryptography;
public static class SecureDelete
{
public static void OverwriteAndDelete(string path)
{
if (!File.Exists(path)) return;
var fi = new FileInfo(path);
var len = fi.Length;
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.None))
{
var rng = RandomNumberGenerator.Create();
var buf = new byte[8192];
long written = 0;
while (written < len)
{
rng.GetBytes(buf);
var toWrite = (int)Math.Min(buf.Length, len - written);
fs.Write(buf, 0, toWrite);
written += toWrite;
}
fs.Flush(true);
}
File.Delete(path);
}
}
実運用テンプレート(README / 手順書にそのまま載せられる文例)
- アンインストール前に実施:アプリを起動 → 設定 → セキュリティ → 「TPM データを削除」を実行(または
cleanup-tpm-keys.ps1を管理者で実行)。 - 削除完了のメッセージを確認。必要に応じてログを保存。
- アプリのアンインストール:「アプリと機能」または配布基盤からアンインストール。
- (組織管理)自動化シナリオでは、管理者が「TPM クリーン → パッケージ削除」の順で一括実行。
まとめ(実務ポイントの再掲)
- MSIX だけでは TPM データは消えない――仕様通りの挙動です。
- アプリ内の削除機能 or スクリプトを提供し、アンインストール前に実行させる導線を用意。
- 接頭辞+台帳で誤削除を防ぐ設計を徹底。CNG/NCrypt で確実に削除。
- 組織配布では Intune 等でTPM クリーン → Remove-AppxPackageの連続ジョブを構成。
Clear-Tpmは最後の切り札。通常運用から外し、影響(BitLocker 等)を理解したうえで限定使用。

コメント