C#定石 - レジストリ操作

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

## レジストリ一覧

レジストリ操作の枠組みとして、ファイル一覧と同様な、レジストリ一覧について記載します。

::: note
Intel CPU の場合、32bit レジストリは Wow6432Node ですが、ARM CPU の場合 WowAA32Node も存在します。
参考 - [レジストリ リダイレクター](https://learn.microsoft.com/ja-jp/windows/win32/winprog64/registry-redirector)

ARM CPU Windows 利用経験がないので、以降は Intel CPU についての記載とします。
:::

### RegistryView

64bit OS の場合、HKEY_LOCAL_MACHINE\Software、および、HKEY_CLASSES_ROOT には、64bit レジストリと 32bitレジストリ（Wow6432Node）が存在します。
WOW64で動作する 32bit 動作プロセスは、HKEY_LOCAL_MACHINE\Software にアクセスすると、HKEY_LOCAL_MACHINE\Software\Wow6432Node にリダイレクトされます。
詳細 - [複数のプロセッサ アーキテクチャに対する Windows on Windows（WOW）サポートを含む Windows インストールの影響を受けるレジストリ キー](https://learn.microsoft.com/ja-jp/windows/win32/winprog64/shared-registry-keys)

| 参照先                       | 32bit プロセス リダイレクト先             |
| --------------------------- | --------------------------------------- |
| HKEY_LOCAL_MACHINE\SOFTWARE | HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node |
| HKEY_CLASSES_ROOT           | HKEY_CLASSES_ROOT\Wow6432Node           |


RegistryView を利用すると、このようなリダイレクトを抑止して、特定のレジストリを操作ができます。
下表のように、32bit プロセスから 64bit レジストリアクセス、64bit プロセスから 32bit レジストリアクセスが可能です。

| RegistryView | 64bitOSで64bit動作 | 64bitOSで32bit動作 | 32bitOS         |
| ------------ | ----------------- | ------------------ | --------------- |
| Default      | 64bit レジストリ   | 32bit レジストリ    | 32bit レジストリ |
| Registry32   | 32bit レジストリ   | 32bit レジストリ    | 32bit レジストリ |
| Registry64   | 64bit レジストリ   | 64bit レジストリ    | 32bit レジストリ |

https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.win32.registrykey.openbasekey/

::: note
WIN32API - RegOpenKeyEx / RegCreateKeyEx 利用時は、KEY_WOW64_32KEY もしくは KEY_WOW64_64KEY を指定となります。

参考 - [代替レジストリ ビューへのアクセス](https://learn.microsoft.com/ja-jp/windows/win32/winprog64/accessing-an-alternate-registry-view)
:::


### サンプルコード

RegistryView を指定することで、該当プロセス 32bit/64bit によらず、特定レジストリの一覧を取得するコードです。

```CSharp
using Microsoft.Win32;
```

```CSharp
string regPath = @"Software\Microsoft\MediaPlayer";
using (var regBase = RegistryKey.OpenBaseKey(
  RegistryHive.LocalMachine, RegistryView.Registry64))
{
  int count = RegValueUnderRegKey(regBase, regPath);
}
```

```CSharp
private int RegValueUnderRegKey(RegistryKey regBase, string regPath)
{
  int count = 0;

  // 参照のみの場合 OpenSubKey、更新する場合 CreateSubKey
  // CreateSubKey を利用すると 対象サブキーが存在しないと作成します
  using (var regKey = regBase.OpenSubKey(regPath))
  {
    if (regKey == null)
    {
      // 対象サブキーなし - TODO
    }
    else
    {
      // 対象サブキーの子サブキーも処理する場合はコメントを外す
      // var keyNames = regKey.GetSubKeyNames().OrderBy(x => x);
      // foreach (var keyName in keyNames)
      // {
      //   count += RegValueUnderRegKey(regBase, regPath + "\\" +  keyName);
      // }

      // 変数一覧
      var names = regKey.GetValueNames().OrderBy(x => x);
      foreach (var name in names)
      {
        // TODO
        // var obj = regKey.GetValue(name);       // 値取得
        // var kind = regKey.GetValueKind(name);  // データ型取得

        count++;
      }
    }
  }
  return count;
}
```

## 取得

レジストリにもアクセス権限が存在します。
基本的には、下記アクセス権限が設定されています。

| 対象              | 管理者権限所有ユーザ  | 一般ユーザ       |
| ----------------- | ------------------- | --------------- |
| HKEY_CURRENT_USER | フルコントロール     | フルコントロール  |
| 上記以外           | フルコントロール     | 読み取り権限のみ  |

::: note
RegistryKey.OpenSubKey（読み取り）、RegistryKey.CreateSubKey（更新）で、該当操作のアクセス権限がない場合は、SecurityException の例外が発生します。
:::

::: note
一部レジストリは、一般ユーザに対して読み取り権限も与えていないサブキーもあります。
:::

### HKEY_CURRENT_USER 以外

設定されている変数/値は、システム全体に対しての設定値で、システムツール、および、インストーラで設定されます。
一般ユーザの場合、基本的に読み取り権限のみとなっているので、RegistryKey.OpenSubKey を用いてアクセスします。

サブキー/変数の存在有無を確認後、対象のデータ型で値を確認という手順は以下の通りです。

- RegistryKey.OpenSubKey で null 取得の場合は、対象サブキーなし
- RegistryKey.GetValue で null 取得の場合は、対象変数なし
- RegistryKey.GetValueKind でデータ型を取得して、該当データ型で値確認


```CSharp
string path = @"Software\Foo\Bar";
// 参照のみなので OpenSubKey
using (var regKey = Registry.LocalMachine.OpenSubKey(path))
{
  if (regKey == null)
  {
    // 対象サブキーなし - TODO
  }
  else
  {
    // 変数/値の確認
    CheckRegValue(regKey, "Hoge");
    CheckRegValue(regKey, "piyo");
  }
}
```

```CSharp
// 変数/値の確認
private void CheckRegValue(RegistryKey regKey, string name)
{
  // 対象変数の値取得
  var obj = regKey.GetValue(name);
  if (obj == null)
  {
    // 対象変数なし - TODO
  }
  else
  {
    // 対象変数のデータ型取得
    var kind = regKey.GetValueKind(name);
    if (kind == RegistryValueKind.String
     || kind == RegistryValueKind.ExpandString)
    {
      var value = obj as string;
       // TOOO
    }
    else if (kind == RegistryValueKind.DWord)
    {
      var value = (Int32)obj;
      // TODO
    }
    else if (kind == RegistryValueKind.QWord)
    {
      var value = (Int64)obj;
      // TODO
    }
    else if (kind == RegistryValueKind.MultiString)
    {
      var values = obj as string [];
      // TODO
    }
    else if (kind == RegistryValueKind.Binary)
    {
      var values = obj as byte [];
      // TODO
    }
    else
    {
      // TODO
    }
  }
}
```

サブキー/変数の存在が前提で、データ型が自明の場合は、シンプルな記述にできます。

```CSharp
string path = @"Software\Foo\Bar";
using (var regKey = Registry.LocalMachine.OpenSubKey(path))
{
  // システムツール、および、インストーラでサブキー/変数/値を設定するので、
  // 対象サブキー/変数（設定値）は存在することが前提
  if (regKey != null)
  {
    var hoge =  regKey.GetValue("Hoge") as string;
    var piyo =  (Int32)regKey.GetValue("Piyo");
  }
}
```

サブキー内で参照する変数が１つの場合には、下記コードにもできます。

```CSharp
string root = "HKEY_LOCAL_MACHINE";  // Registry.LocalMachine
string path = root + "\\" + @"Software\Foo\Bar";
var hoge =  Registry.GetValue(path, "Hoge", null) as string;
```

### HKEY_CURRENT_USER

パッケージソフトで、ユーザ毎の設定情報を、レジストリ KEY_CURRENT_USER で管理するケースがあります。
このようなケースでは、

- 対象サブキーに更新権限がある
- 対象サブキーが存在しない場合は、作成しても構わない
- 変数のデータ型が自明
- 変数が存在しない場合は、既定値取得

という仕様で良いので、下記コードを用いることがありました。

```CSharp
string path = @"Software\Foo\Bar";
// 対象サブキーがない場合は、作成しても構わないので CreateSubKey 利用
using (var regKey = Registry.CurrentUser.CreateSubKey(path))
{
  // サブキー作成しているので、サブキーが存在しない場合の既定値も以下で対処可能
  var hoge =  regKey.GetValue("Hoge", "既定値") as string;
  var piyo =  (Int32)regKey.GetValue("Piyo", 512);  // 512 は Int32 の規定値
}
```

サブキー内で参照する変数が１つの場合には、下記コードにもできます。

```CSharp
string root = "HKEY_CURRENT_USER";  // Registry.CurrentUser
string path = root + "\\" + @"Software\Foo\Bar";
string defaultValue = "既定値";
var hoge =  (Registry.GetValue(path, "Hoge", defaultValue) ?? defaultValue) as string;
```

::: note
Registry.GetValue では RegistryKey.OpenSubKey を内部で実施するので、該当サブキーが存在しない場合は、サブキーを作成せず null 取得となります。
このため、null 合体演算子を利用して、既定値の記載が必要となります。 
:::

### ExpandString

RegistryValueKind.ExpandString（REG_EXPAND_SZ：環境変数を含む展開可能な文字列値）の扱いは特殊です。

RegistryValueOptions.DoNotExpandEnvironmentNames を指定しない場合、環境変数を展開した値を取得します。
環境変数を展開せず取得するには、RegistryValueOptions.DoNotExpandEnvironmentNames の指定が必要で、このように取得した値の環境変数展開は ExpandEnvironmentVariables で可能です。


```CSharp
string path = "Environment";
string name = "TEMP";

using (var regKey = Registry.CurrentUser.OpenSubKey(path))
{
  if (regKey != null)
  {
    if (regKey.GetValueKind(name) == RegistryValueKind.ExpandString)
    {
      // 環境変数を展開して取得
      var foo = regKey.GetValue(name) as string;
      // → C:\Users\hoge\AppData\Local\Temp

      // 環境変数を展開せず取得
      var bar = regKey.GetValue(name, null,
                         RegistryValueOptions.DoNotExpandEnvironmentNames) as string;
      // → %USERPROFILE%\AppData\Local\Temp
      if (bar != null)
      {
        // 環境変数を展開
        var baz = Environment.ExpandEnvironmentVariables(bar);
        // → C:\Users\hoge\AppData\Local\Temp
      }
    }
  }
}
```
## 設定

RegistryKey.OpenSubKey でなく、RegistryKey.CreateSubKey を利用して、RegistryValueKind に一致した型の値を設定します。

```CSharp
string path = @"Software\Foo\Bar";
// 更新時は CreateSubKey 利用
using (var regKey = Registry.CurrentUser.CreateSubKey(path))
{
  regKey.SetValue("Hoge", "値1", RegistryValueKind.String);
  regKey.SetValue("Piyo", 512", RegistryValueKind.DWord);
}
```

サブキー内で設定する変数が１つの場合には、下記コードにもできます。

```CSharp
string root = "HKEY_CURRENT_USER";  // Registry.CurrentUser
string path = root + "\\" + @"Software\Foo\Bar";
Registry.SetValue(path, "Hoge", "値1", RegistryValueKind.String);
```

::: note
Registry.SetValue では RegistryKey.CreateSubKey を内部で実施するので、該当サブキーが存在しない場合は、サブキー作成した後、値を設定します。
:::


## 削除

### 変数削除

```CSharp
string path = @"Software\Foo\Bar";
// 更新時は CreateSubKey 利用
using (var regKey = Registry.CurrentUser.CreateSubKey(path))
{
  regKey.DeleteValue("hoge", false);   // 対象が存在しない場合、例外を発生させない
  regKey.DeleteValue("piyo", false);   // 対象が存在しない場合、例外を発生させない
}
```

### サブキー削除

サブキー削除は、RegistryKey.DeleteSubKey、RegistryKey.DeleteSubKeyTree の２つのメソッドがあります。
前者は、対象サブキーに子サブキーが存在すると削除に失敗しますが、後者は、サブキーとその子サブキーを再帰的に削除します。

```CSharp
string path = @"Software";
// 更新時は CreateSubKey 利用
using (var regKey = Registry.CurrentUser.CreateSubKey(path))
{
  regKey.DeleteSubKeyTree("Foo", false); // 対象が存在しない場合、例外を発生させない
}
```

## セキュリティ権限付与
パッケージソフトとして、更新可能な設定情報をレジストリで管理する場合、HKEY_CURRENT_USER\Software を用いて、ユーザ毎に管理することが一般的です。

しかしながら、全てのユーザを対象とする設定情報で、かつ、アプリから更新も可能という要求仕様に出会うこともありました。
この場合、HKEY_LOCAL_MACHINE\Software を利用することになりますが、更新には管理者権限が必要となります。
アプリに管理者権限付与するのは望ましくないので、インストール処理（管理者権限有り）で、対象レジストリキーを EveryOne フルコントールとすることもありました。

::: note
そもそも、レジストリ利用でなく、可搬性が高い XML 利用でも良いはずですが、、、
XML 利用するにしても、EveryOne フルコントールのフォルダを用意することになるので、手間は同じですね。
:::

```CSharp
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.Win32;
```

```CSharp
string regPath = @"Software\Foo\Bar\Baz";
using (var regBase = RegistryKey.OpenBaseKey(
  RegistryHive.LocalMachine, RegistryView.Registry32))
{
  SetFullAccessForRegistry(regBase, regPath);
}
```

```CSharp
private bool SetFullAccessForRegistry(RegistryKey regBase, string regPath)
{
  bool bResult = false;
  
  // 対象キーが存在しなければ作成する
  using (var regKey =  regBase.CreateSubKey(regPath))
  {
    // Everyone
    var sid = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
    if (sid != null)
    {
      var account = sid.Translate(typeof(NTAccount)) as NTAccount;
      if (account != null)
      {
        RegistrySecurity rs = regKey.GetAccessControl();
        RegistryAccessRule rar = new RegistryAccessRule(
                    account.ToString(),
                    RegistryRights.FullControl,
                    InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
                    PropagationFlags.None,
                    AccessControlType.Allow);
        // AddAccessRule と SetAccessRule は仕様が異なる
        rs.AddAccessRule(rar);
        regKey.SetAccessControl(rs);

        // 正常終了
        bResult = true;
      }
    }
  }
  return bResult;
}
```

https://learn.microsoft.com/ja-jp/dotnet/api/system.security.principal.securityidentifier

https://learn.microsoft.com/ja-jp/dotnet/api/system.security.accesscontrol.registrysecurity