MiniAPI(十六)：データ保護

ウェブにおけるセキュリティは永遠のテーマであるため、ASP.NET Coreデータ保護はシンプルで使いやすい暗号化APIを提供し、データ保護、キー管理、ローテーションを行うことができます。
ASP.NET Coreのデータ保護は、ネイティブのキーに基づいて暗号化キーを生成し、そのキーでデータの暗号化および復号を行います。キーが異なる場合、復号は失敗します。デフォルトでは、このキーの有効期限は90日ですが、この値は変更することができます。
デフォルトでデータ保護キーが保存される場所は、C:\Users\ユーザー\AppData\Local\ASP.NET\DataProtection-Keysです。例えば、key-a2b3132b-444b-4cfa-8530-922b7e991cd9.xmlというXMLファイルがあり、このキーの作成日、有効開始日、有効終了日、およびデータの暗号化および復号に使用される名前空間、暗号化方式などの情報が記録されています。

```xml
<?xml version="1.0" encoding="utf-8"?>
<key id="a2b3132b-444b-4cfa-8530-922b7e991cd9" version="1">
  <creationDate>2022-02-10T13:41:14.7492868Z</creationDate>
  <activationDate>2022-02-10T13:41:14.7421157Z</activationDate>
  <expirationDate>2022-05-11T13:41:14.7421157Z</expirationDate>
  <descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
    <descriptor>
      <encryption algorithm="AES_256_CBC" />
      <validation algorithm="HMACSHA256" />
      <encryptedSecret decryptorType="Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiXmlDecryptor, Microsoft.AspNetCore.DataProtection, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60" xmlns="http://schemas.asp.net/2015/03/dataProtection">
        <encryptedKey xmlns="">
          <!-- This key is encrypted with Windows DPAPI. -->
          <value>AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAAZeuIp6hZUGsfTgUuNoSSAAAAAACAAAAAAAQZgAAAAEAACAAAACpVhXOBzCWDrZmD13HwR6U3qHk7O2Pki1vPEBrtOJlrQAAAAAOgAAAAAIAACAAAAAPc2zwN10L4IVtN7tFsdQV0Cx7giHlNrWI6heArHHFt1ABAAACCCC8BhZwQ32ZZ67nBEi/tZS+HagViuS/xYtlUJOfzkJOmWg28KkBR2vM6Jo1Y1OY/AbIx6EhbGvZUkdL9aeGBy6A0GBS/3VC4/X8KV3sihRPhb1n924slVds0Y9p7J3p6sCLbhvh0ohhBe1pAENr1XkUnd2Ve4JQ0gVVgQ7vFJOonGGXGQ52dzmITzkJLYIqwNtksA31OmJUlJG7KJnrDEofKjQynvj9gXnOVnMpfHNk6v8v96WDlK7n9Ax3o/W238E7FtOVBKTNoIFWwyc40MR25IrkQdtMZ7HrODY1VRL9nuuexbVXq+5mt5QOyVgvZ1RK0sCwaBB3FSHwKmjskk+WpHXZpi3hjLx82F1gCpatSma01zDEDte0LZHRG9pVYgUXRwUaXX3G0uPuI2mXNpN57qIGZhCJC37cACIzJQ5NxuS+n9Rs6SjVykn78LBAAAAAkxOreUFzysrk5EeldARfOulsq/9OT2w/AFU9sRvPWgZPOieKcdAfIuNF09FgcyquX6IcNuydPn46Uy+saHia0w==</value>
        </encryptedKey>
      </encryptedSecret>
    </descriptor>
  </descriptor>
</key>
```

データ保護のコードの使用も非常に簡単で、DataProtectionサービスを注入するだけです。サービスを使用する際には、IDataProtectionProviderを介してデータ保護オブジェクトを作成し、作成時に目的の文字列パラメータを追加することで、異なる目的の文字列で暗号化されたデータを隔離することができます。このため、キーとこの目的の文字列は隔離機能を果たすことができます。

```C#
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);
//デフォルトの90日を10日に変更する
builder.Services.AddDataProtection().SetDefaultKeyLifetime(TimeSpan.FromDays(10));

var app = builder.Build();

app.MapGet("/encrypt/{str}", (IDataProtectionProvider provider, ILogger<Program> logger, string str) =>
{
    var protector = provider.CreateProtector("a.b.c");
    var sec = protector.Protect(str);
    logger.LogInformation(sec);
    return "加密：" + sec;
});

app.MapGet("/decrypt/{sec}", (IDataProtectionProvider provider, ILogger<Program> logger, string sec) =>
{
    var protector = provider.CreateProtector("a.b.c");
    var str = protector.Unprotect(sec);
    logger.LogInformation(str);
    return "解密：" + str;
});

app.Run();
```

上記のコードは単一マシンのデプロイを実装したものです。クラスタデプロイ、例えば、k8s内の異なるpodで生成されたキーはそれぞれ自身のpodに保存されています。この場合、外部からのアクセスはランダムに割り当てられるため、頻繁に復号失敗が発生する場合があります。この問題を解決するためには、キーの集中管理が必要になります。Redisやデータベースを使用することができますが、ここではSqlServerが使用されています。まず、キーを保存するためのテーブルを作成します。

```
CREATE TABLE [dbo].[DataProtectionKeys1](
  [ID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY ,
  [FriendlyName] [varchar](64) NULL,
  [Xml] [text] NULL
)
```

キーが保存された結果は以下のとおりです。
![alt 画像](https://mmbiz.qpic.cn/mmbiz_png/4CAA63nvHRCoRwsLh3DSsLhAyUbnQkPJAD530akjBboCEZvyeO9I0KsfRvIrp7DkpfxKlKZqJ6UCKqBgAibaB0A/640?wx_fmt=png)

ここで、キーの値はローカルファイルに保存されたものと同じです。

この段階で、コードはEFをサポートする必要があり、以下のNuGetパッケージを導入します。
- Microsoft.AspNetCore.DataProtection
- Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer

キーをEF方式で永続化するためには、EFのContextを注入し、データ保護オブジェクトを注入する際に永続化方式を指定する必要があります。

```C#
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<DataProtContext>(options =>
      options.UseSqlServer(builder.Configuration.GetConnectionString("DataProtDB")));

builder.Services.AddDataProtection().PersistKeysToDbContext<DataProtContext>();

var app = builder.Build();

app.MapGet("/encrypt/{str}", (IDataProtectionProvider provider, ILogger<Program> logger, string str) =>
{
    var protector = provider.CreateProtector("a.b.c");
    var sec = protector.Protect(str);
    logger.LogInformation(sec);
    return "加密：" + sec;
});

app.MapGet("/decrypt/{sec}", (IDataProtectionProvider provider, ILogger<Program> logger, string sec) =>
{
    var protector = provider.CreateProtector("a.b.c");
    var str = protector.Unprotect(sec);
    logger.LogInformation(str);
    return "解密：" + str;
});

app.Run();

class DataProtContext : DbContext, IDataProtectionKeyContext
{
    public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }

    public DataProtContext(DbContextOptions<DataProtContext> options)
          : base(options)
    {}
}
```

加密結果：
![alt 画像](https://mmbiz.qpic.cn/mmbiz_png/4CAA63nvHRCoRwsLh3DSsLhAyUbnQkPJTK1xFdRUVsibicZMajwNiadXxFTAGqTVGa2Fa0rCJLdCEwCPQl0hic4ANA/640?wx_fmt=png)

解密結果：
![alt 画像](https://mmbiz.qpic.cn/mmbiz_png/4CAA63nvHRCoRwsLh3DSsLhAyUbnQkPJLpwVpNjyiaWQiczGf3hcYalmmYCCQEbxzR2ibQshTh148Pbe8g0kTILTA/640?wx_fmt=png)

加密文字列に有効期限を設定する場合、生成されたProtectorをTimeLimitedDataProtectorに変換して暗号化および復号を行います。

```
……
app.MapGet("/encrypt/{str}", (IDataProtectionProvider provider, ILogger<Program> logger, string str) =>
{
    var protector = provider.CreateProtector("a.b.c");
    var sec = protector.ToTimeLimitedDataProtector().Protect(str, TimeSpan.FromSeconds(30));
    logger.LogInformation(sec);
    return "加密：" + sec;
});

app.MapGet("/decrypt/{sec}", (IDataProtectionProvider provider, ILogger<Program> logger, string sec) =>
{
    var protector = provider.CreateProtector("a.b.c");
    var str = protector.ToTimeLimitedDataProtector().Unprotect(sec);
    logger.LogInformation(str);
    return "解密：" + str;
});
……
```

暗号化文字列の有効期限が切れて提出された場合、以下のようなエラーが発生します。
![alt 画像](https://mmbiz.qpic.cn/mmbiz_png/4CAA63nvHRCoRwsLh3DSsLhAyUbnQkPJoicyxicrV85k5OMjTyuGPiaQSoNRtBD6iaRy08W01icMVdhEE3libryfEicNA/640?wx_fmt=png)

(Translated by GPT)

元のリンク：https://mp.weixin.qq.com/s?__biz=MzA3NDM1MzIyMQ==&mid=2247484933&idx=1&sn=c90ae51ce67d2d9fe3ee37abb2e66b17&chksm=9f00592fa877d039fe070858cada32a63547ee4b623c09dd0b7b4f259ae2a788db01de5648ce&token=541757603&lang=zh_CN#rd