MagicOnionで独自暗号化処理を追加する

# はじめに

MagicOnion については[こちら](http://tech.cygames.co.jp/archives/3181/)をご覧ください。
今回は MagicOnion で独自暗号化処理を追加してみようと思います。独自暗号を挟む用途としては、ソーシャルゲームなどにおいてユーザー本人によるチート行為への対策などを想定しています。

# gRPC Interceptor

MagicOnion において、暗号化のような MessagePack の byte 列に変換した後にすべき処理は gRPC の Interceptor という仕組みを使うことで実現できます。
Interceptor を使うとそれぞれの RPC の直前・直後に処理を挟むことができるようになります。
これは gRPC の機能なので、以下の内容は MagicOnion のみに限らず、素の gRPC でも使える内容になります。

## 実装

自作の Interceptor は [Grpc.Core.Interceptors.Interceptor](https://grpc.io/grpc/csharp/api/Grpc.Core.Interceptors.Interceptor.html) クラスを継承します。
サーバ側・クライアント側それぞれについて、RPC 種類ごとにメソッドが用意されているので、必要なものだけを override して実装することになります。

- サーバ側
 - UnaryServerHandler
 - ServerStreamingServerHandler
 - ClientStreamingServerHandler
 - DuplexStreamingServerHandler
- クライアント側
 - BlockingUnaryCall
 - AsyncUnaryCall
 - AsyncServerStreamingCall
 - AsyncClientStreamingCall
 - AsyncDuplexStreamingCall

以下では Unary の書き方のみ記載します。

### サーバ側

以下のようにリクエスト、レスポンスそれぞれに処理を挟むことができます。

```csharp
public class HogeInterceptor : Interceptor
{
    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation
    ) {
        /* ここで request を加工する */

        // 通常処理
        var response = await base.UnaryServerHandler(request, context, continuation);

        /* ここで response を加工する */

        return response;
    }
}
```

### クライアント側

クライアント側は戻り値が Task ではないので、レスポンスに処理を挟むのが若干面倒になっています。
一旦 AsyncUnaryCall を受け取ってから、新しい AsyncUnaryCall に諸々をコピーして返す必要があります。

```csharp
public class HogeInterceptor : Interceptor
{
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation
    )
    {
        /* ここで request を加工する */

        var call = base.AsyncUnaryCall(request, context, continuation);

        var responseTask = call.ResponseAsync.ContinueWith(x => 
        {
            var response = x.Result;

            /* ここで response を加工する */

            return response;
        });

        return new AsyncUnaryCall<TResponse>(
            responseTask,
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose
        );
    }
}
```

## 適用方法

### サーバ側

gRPC サーバの起動時、 Services に Interceptor を挟んだ ServerServiceDefinition を渡すことで機能するようになります。

```csharp
var service = MagicOnionEngine.BuildServerServiceDefinition();
var server = new Grpc.Core.Server
{
    // ここで Interceptor を挟んだ ServerServiceDefinition を渡す
    Services = { service.ServerServiceDefinition.Intercept(new HogeInterceptor()) },
    // 挟まない場合はこう
    // Services = { service },
    Ports = { new ServerPort("localhost", 12345, ServerCredentials.Insecure) }
};
server.Start();
```

### クライアント側

MagicOnionClient.Create の際に Channel そのままではなく Interceptor を挟んだ CallInvoker を渡すことで機能するようになります。

```csharp
var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure);
// ここで Interceptor を挟んだ CallInvoker を生成
var invoker = channel.Intercept(new HogeInterceptor());
var client = MagicOnionClient.Create<IMyFirstService>(invoker);
// 挟まない場合はこう
// var client = MagicOnionClient.Create<IMyFirstService>(channel);
```

# 暗号化

ようやく本題の暗号化ですが、上記のとおりに実装するだけのでそのままコードを載せます。
※暗号アルゴリズムについてはどうでもいいので省略します。

```csharp
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Core.Interceptors;

public class CipherInterceptor : Interceptor
{
    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation
    ) => Encrypt(await base.UnaryServerHandler(Decrypt(request), context, continuation));

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation
    ) {
        var call = base.AsyncUnaryCall(Encrypt(request), context, continuation);
        return new AsyncUnaryCall<TResponse>(
            call.ResponseAsync.ContinueWith(x => Decrypt(x.Result)),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose
        );
    }

    private T Encrypt<T>(T data)
    {
        /* 略 */
    }

    private T Decrypt<T>(T data)
    {
        /* 略 */
    }
}
```

このクラスをサーバ・クライアントで共有することで、同じ暗号アルゴリズムを使っていることが簡単に保証できていい感じです。

### まとめ

MagicOnion でも gRPC の Interceptor を使うことができます。今回はそれを用いて独自の暗号化処理を挟みました。