この記事は「大石泉すき」アドベントカレンダー 4日目の記事となります。
4日目は、「大石泉すきを暗号化しよう!」とします。
2019/12/04 21:37追記
当初「復号」を「復号化」と記載していたため、修正いたしました。ご指摘いただき、ありがとうございます。
何をしたいのか
「大石泉すき」という言葉を、一度暗号に直し、再度意味の通る文字に戻したい(復号したい)です。
今回は、公開鍵暗号を用いてこれを行っていこうと思います。
公開鍵暗号とは
仕組み自体は本筋ではないので、詳しくは下記の記事などを参照してください。
公開鍵暗号方式とは、暗号化と復号に別々の鍵を用いる暗号方式である。「非対称鍵暗号方式」とも呼ばれる。
~(中略)~
公開鍵暗号方式では、「暗号文を作り出す鍵」と「暗号文を元に戻す鍵」が異なる。暗号通信を行いたい人は、まず独自に2つの鍵のペアを作成する。同時に生成された一対の鍵のうち一方を公開鍵として公開し、他方を秘密鍵として厳重に管理する。送信者は受信者の公開鍵で暗号文を作成して送る。受信者は、自分の秘密鍵で受け取った暗号文を復号する。
引用元:@IT 公開鍵暗号方式とは
AliceとBobという名前の二者間の通信を例に出すと、
- Bobは、Aliceに**秘密の文字列(平文)**を送りたい
- Bobは、Aliceが公開している公開鍵(誰でも知ることが出来る)を使って、文字列を暗号化する
- Aliceは、Bobから暗号化した文字列を受け取る
- Aliceは、自分が持っている秘密鍵(Aliceしか知らない)で文字列を復号する
- Aliceは、Bobから送られた秘密の文字列を受け取る
こんな風に、文字列の送受信が秘密に行えます。
(実際に利用される場合は、公開鍵が本当に正しいか、Aliceが所持しているかを証明する第三者機関が居たりしますが、今回は割愛します)
環境
今回、二者間の通信を表現するために、以下のような構成でソースコードを書きました。
C#で書く場合、AliceとBobでソリューションを分けてください。
Alice(メッセージ受信側)
- 役割
- HTTPサーバ上で公開鍵を公開する
- 受信した暗号オブジェクトを復号する
- 言語・フレームワーク
- C# 8.0
- ASP .net Core 3.0
Bob(メッセージ送信側)
- 役割
- Aliceが公開している公開鍵を入手し、秘密の文字列を暗号化する
- 暗号化した文字列をAliceに送信する
- 言語・フレームワーク
- C# 8.0
- .net Core 3.0 コンソールアプリケーション
実装
暗号化・復号の実装
下記のライブラリを使いました。
使い方は下記の記事を参照。
更新日付を見る限り .net Framework用のライブラリを使っての紹介記事だと思われるが、.net Core用のライブラリでも同様の使い方で暗号化・復号が可能。
使い方は大体こんな感じ。暗号文・復号文共にbyte[]配列で表される。
// 暗号化
public static byte[] Encrypt(byte[]) bytes, string publickey)
{
// PEMフォーマットの公開鍵を読み込んで KeyParam を生成
var publicKeyReader = new PemReader(new StringReader(publickey));
var publicKeyParam = (AsymmetricKeyParameter)publicKeyReader.ReadObject();
var RSA = new Pkcs1Encoding(new RsaEngine());
// RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号)
RSA.Init(true, publicKeyParam);
// 暗号化対象のバイト列・長さを渡し、暗号化した結果のバイト列を受け取る
byte[] encrypted = RSA.ProcessBlock(bytes, 0, bytes.Length);
return encrypted;
}
// 復号
public byte[] Decrypto(byte[] cipher, string privateKey)
{
// PEMフォーマットの秘密鍵を読み込んで KeyParam を生成
var privateKeyReader = new PemReader(new StringReader(privateKey));
var privateKeyParam = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject();
var RSA = new Pkcs1Encoding(new RsaEngine());
// RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号)
RSA.Init(false, privateKeyParam.Private);
// 復号対象のバイト列・長さを渡し、復号した結果のバイト列を受け取る
var decrypto = RSA.ProcessBlock(cipher, 0, cipher.Length);
return decrypto;
}
BouncyCastle入手方法
- ソリューションエクスプローラから「依存関係」を右クリック
- 「NuGet パッケージの管理」をクリック > 「参照」をクリック
- 「BouncyCastle.NetCore」を検索欄に入力し、出てきたものをインストール
- 「BouncyCastle」は .net Framework用なので注意
鍵ペアの生成
動確検証用の公開鍵/暗号鍵のペアは、このサイトで生成しました。
https://travistidwell.com/jsencrypt/demo/index.html
個別実装
メッセージ受信側(HTTPサーバ)
/// <summary>
/// 暗号byte[]配列の復号
/// </summary>
/// <returns></returns>
[HttpPost("")]
public async System.Threading.Tasks.Task<string> DecryptoAsync()
{
byte[] encrypto;
using (var ms = new MemoryStream(2048))
{
await Request.Body.CopyToAsync(ms);
encrypto = ms.ToArray(); // returns base64 encoded string JSON result
}
var cert = new Cert();
var decryptoByte = cert.Decrypto(encrypto, Cert.PRIVATE_KEY);
// ログ
_logger.LogInformation($"Decrypto [{Encoding.UTF8.GetString( decryptoByte )}]");
return Encoding.UTF8.GetString( decryptoByte );
}
/// <summary>
/// 公開鍵
/// </summary>
/// <returns></returns>
[HttpGet("Alice/cert")]
public string GetCert()
{
return Cert.PUBLIC_KEY;
}
- {ルートパス}/Alice/certに、Getリクエスト:公開鍵を生のstring型で返却する
- ルートパスに、bodyに生のbyte配列(暗号文)を添付しPostリクエスト:リクエストのbyte配列を復号したbyte配列を、文字列に変換する。
- 今回は、どのように変換したのか分かるように、stringで結果を返却する実装にした
class Cert
{
// 実際の運用時はハードコーディングせず、セキュアな場所に保存し逐一読み込むこと
// Generate by https://travistidwell.com/jsencrypt/demo/index.html
internal static readonly string PUBLIC_KEY = @"(略)";
// 実際の運用時はハードコーディングせず、セキュアな場所に保存し逐一読み込むこと
// Generate by https://travistidwell.com/jsencrypt/demo/index.html
internal static readonly string PRIVATE_KEY = @"(略)";
internal Pkcs1Encoding RSA { get; }
public Cert()
{
RSA = new Pkcs1Encoding(new RsaEngine());
}
/// <summary>
/// 対称鍵暗号で暗号文を復号する
/// </summary>
/// <param name="cipher">平文の文字列</param>
/// <param name="privatekey">秘密鍵</param>
/// <returns>復号された文字列</returns>
public byte[] Decrypto(byte[] cipher, string privateKey)
{
// PEMフォーマットの秘密鍵を読み込んで KeyParam を生成
var privateKeyReader = new PemReader(new StringReader(privateKey));
var privateKeyParam = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject();
var RSA = new Pkcs1Encoding(new RsaEngine());
// RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号)
RSA.Init(false, privateKeyParam.Private);
// 復号対象のバイト列・長さを渡し、復号した結果のバイト列を受け取る
var decrypto = RSA.ProcessBlock(cipher, 0, cipher.Length);
return decrypto;
}
}
- 単純な復号処理。公開鍵・秘密鍵は絶対にハードコーディングしないこと
メッセージ送信側(コンソールアプリ)
static void Main()
{
// HttpClientを使うための準備。今回はあまり関係ない
// HTTPConnectionFactoryを使うため、DI設定を行う
var serviceCollection = new ServiceCollection()
.AddHttpClient() // IHttpClientFactoryの依存設定
.AddSingleton<IHttpConnection, HttpConnectionSample>() // IHTTPConnectionの依存設定
.BuildServiceProvider();
// DI設定済みのIHttpConnectionを実装したクラスを取得
var connector = serviceCollection.GetService<IHttpConnection>();
// 大石泉すき
string plainText = "大石泉すき";
Console.WriteLine($"PlainText\r\n{plainText}\r\n");
// サーバから公開鍵を取得する
// SendGetメソッドの中身はただのHttpClient.GetAsyncです
var publicKey = connector.SendGet($"https://{メッセージ受信側HTTPサーバのIP:Port}/Alice/cert").Result;
// RSA暗号標準オブジェクト(PKCS#1)を生成
var rsa = new Pkcs1Encoding(new RsaEngine());
// 暗号化
var encrypted = Encrypt(plainText, publicKey, rsa);
// byte配列は化けるのでBase64でエンコードしておく
Console.WriteLine($"Encrypted(Base64 Encoded)\r\n{Convert.ToBase64String(encrypted)}\r\n");
// 暗号文(配列)を復号するべく、サーバに暗号文を送信
// SendPostメソッドの中身はただのHttpClient.PostAsyncです
var decrypted = connector.SendPost($"https://{メッセージ受信側HTTPサーバのIP:Port}/rsaremote/", encrypted).Result;
// サーバで復号した結果を表示
Console.WriteLine($"Decrypted\r\n{decrypted}\r\n");
}
- 公開鍵取ってきてーの暗号化してーの復号してもらいーののコントローラクラス
- 「大石泉すき」を知っているのはBobだけ。暗号化してAliceに伝わるだろうか。
/// <summary>
/// 公開鍵で文字列を暗号化する
/// </summary>
/// <param name="text">平文の文字列</param>
/// <param name="publickey">Pem形式の公開鍵</param>
/// <returns>暗号化されたByte</returns>
public static byte[] Encrypt(string text, string publickey, Pkcs1Encoding rsa)
{
var bytes = Encoding.UTF8.GetBytes(text);
// PEMフォーマットの公開鍵を読み込んで KeyParam を生成
var publicKeyReader = new PemReader(new StringReader(publickey));
var publicKeyParam = (AsymmetricKeyParameter)publicKeyReader.ReadObject();
// RSA暗号オブジェクトを初期化(第1引数 true は「暗号化」を示す)
rsa.Init(true, publicKeyParam);
// 対象のバイト列を渡し暗号化した結果のバイト列を受け取る
byte[] encrypted = rsa.ProcessBlock(bytes, 0, bytes.Length);
return encrypted;
}
- 単純な暗号化処理。公開鍵・秘密鍵は今回クライアントは持っていない
動作確認
サーバ側を起動させた状態で、クライアント側を実行
PlainText
大石泉すき
Encrypted(Base64 Encoded)
W2joxjgxL+Q6CtCYaSGCzpx4fJYspCb7KRI/2Ddlnt//70o0R/039Hx6R2fywqCEF0Q21MqpF4/BbjzDM8lAKJgPEIFx5Gp2kYBO08B6bjYdrhSPgIeWEIj7ulwZPO4TD+G5bGwrZn/ogapQfUbTY748B49h1/d4t0IowxRartc=
Decrypted
大石泉すき
RsaServer.Controllers.RsaRemoteController: Information: Decrypto [大石泉すき]
参考資料
-
Accepting Raw Request Body Content in ASP.NET Core API Controllers
- Asp .net Coreで生のByte配列を取得するのに躓いたので、半日くらいこの文書を探していました。