マグネテック備忘録

Flutterアプリ開発の備忘録

【Flutter】cryptograhyで暗号化(AES-GCM256)



注意

  • 本記事では暗号化パッケージの紹介&基本的な使い方です。
  • 私自身セキュリティに詳しくないので、紹介した運用方法は安全性に関して問題があるかもしれません。
  • 本記事の紹介した手法で被ったトラブルなどに関しては一切責任を負いかねますので、ご容赦ください。





概要

  • 以前ローカルデータの保存方法の記事を書いたのですが、よくよく考えたら生データを置くと改ざんができるんじゃねと思ったので、対策のために暗号化の方法を少し調べてみました。
  • そこで今回はcryptographyというパッケージを使うことにしました(公式ページ:cryptography | Dart Package)
  • 似たようなパッケージにcryptoがありますが、こちらはハッシュ化(改ざん検知などに使える)であり暗号化とは別のものになるみたいです。



準備

パッケージの追加

dependencies:
  cryptography: ^2.7.0

インポート文

import 'package:cryptography/cryptography.dart';





基本的な使用方法

暗号化アルゴリズムの選択

  • デジタル署名やRSA(公開鍵暗号)があるみたいですが、今回はよく使われている共通鍵方式のAESを使います。
  • 鍵のビット長は128, 192, 256と3種類あります(長いほど安全だが処理速度が遅い)。
  • 暗号化モードも選択できますが、今回は公式ページのExampleにもあるAES-GCM(256ビット)を使ってみます。
// AesGcm型
final algorithm = AesGcm.with256bits();



暗号鍵の設定

  • 暗号化に使う鍵の値を決定します。
  • 通信相手以外には知られないように管理しましょう。
  • SecretKey型の変数を用意します。
  • 前項で256ビット=32バイトとしたので、1バイト(0~255)の数値を32個並べます。
SecretKey secretKey = SecretKey([
  12, 88, 204, 87, 200, 189, 103, 145, 94, 54, 246, 34, 140, 35, 243, 135, 
  105, 130, 62, 172, 214, 230, 73, 69, 66, 217, 27, 63, 31, 229, 117, 76
]);


  • この数値を用意するのが面倒な場合は、先ほど用意した変数algorithm内の関数newSecretKey()を使えば乱数列が生成されます。
SecretKey secretKey = await algorithm.newSecretKey();


  • ちなみにSecretKey型から数値は以下のようにして取り出せます。
final keyValues = await secretKey.extractBytes();



ノンスの生成

  • 初期値のようなものを用意します。
  • これがないと、ショッピングにおける注文データの暗号文を悪意ある人が知れれば、例え中身が解読できなくても再送信することで注文を繰り返すというリプライ攻撃が可能になるそうです(wikipediaを参考にしました。)
  • こちらは92ビット=12バイトみたいなので、1バイト(0~255)の数値を12個並べます。
List<int> nonce = [216, 125, 240, 232, 12, 183, 60, 105, 74, 25, 20, 46];



鍵のときと同様に面倒なら、先ほど用意した変数algorithm内の関数newNonce()を使えば乱数列が生成されます。

List<int> nonce = algorithm.newNonce();



暗号化(encrypt関数)

  • 先ほど用意したalgorithmの関数encryptを使うことで暗号文を生成できます。
  • 元の文章(List型)、鍵(SecretKey型)、ノンス(List型)を引数にし、SecretBox型の変数を返します。
  • 暗号文はsecretBox.cipherTextとすることで取り出せます。
SecretBox secretBox = await algorithm.encrypt(
  clearTextValues, //List<int>
  secretKey: secretKey,
  nonce: nonce,
);


  • 文字列を暗号化したいときは、utf8.encode()などを使い、String⇒Listの変換をするようにしましょう。
import 'dart:convert';
List<int> clearTextValues = utf8.encode('test');



復号(decrypt)

  • encryptで作成したsecretBoxと鍵を引数にします。
  • AES-GCMは認証機能もあるので、鍵やノンスが暗号生成時から変化している場合などにはエラーが出ます
final List<int> decryptedValues = await algorithm.decrypt(
  secretBox,
  secretKey: secretKey,
);


  • String型で取り出したいときは、以下のようにすればOKです。
import 'dart:convert';
String decryptedText = utf8.decode(decryptedValues);



デモアプリ

  • これまでのコードをまとめたアプリです。

サンプルコード

import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:cryptography/cryptography.dart';

void main() {

  final app = MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      useMaterial3: true,
    ),
    home: const MyHomePage(),
  );

  runApp(app);
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  // 元の文字列
  List<int> clearTextValues = [];

  // 暗号文
  List<int> cipherTextValues = [];

  // 暗号化アルゴリズム
  final algorithm = AesGcm.with256bits();

  // 暗号鍵
  SecretKey secretKey = SecretKey([
    12, 88, 204, 87, 200, 189, 103, 145, 94, 54, 246, 34, 140, 35, 243, 135, 
    105, 130, 62, 172, 214, 230, 73, 69, 66, 217, 27, 63, 31, 229, 117, 76
  ]);

  // ノンス
  List<int> nonce = [216, 125, 240, 232, 12, 183, 60, 105, 74, 25, 20, 46];

  // 暗号文とかをしまっておくもの
  late SecretBox secretBox;

  // 復号した文章
  String decryptedText = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[

            // 鍵の再生成
            ElevatedButton(
              onPressed: () async{
                secretKey = await algorithm.newSecretKey();

                // 鍵の値を表示
                final keyValues = await secretKey.extractBytes();
                print('secretKey = $keyValues');
              }, 
              child: const Text('鍵の再生成')  
            ),

            // ノンスの再生成
            ElevatedButton(
              onPressed: () {
                nonce = algorithm.newNonce();
                print('nonce = $nonce');
              }, 
              child: const Text('ノンスの再生成')  
            ),

            // 暗号化したい文章を入力
            TextField(
              textAlign: TextAlign.center, // 中央寄せ

              onChanged: (text) async{

                // 元の文字列を更新(StringをList<int>に変換)
                setState(() {
                  clearTextValues = utf8.encode(text);
                });
                
                // 暗号化
                secretBox = await algorithm.encrypt(
                  clearTextValues,
                  secretKey: secretKey,
                  nonce: nonce,
                );

                // 暗号文をList<int>⇒String
                setState(() {
                  cipherTextValues = secretBox.cipherText;
                });
              },
            ),

            // 元の文字列を表示
            Text(
              'clearTextValues : $clearTextValues',
            ),

            // 暗号文を表示
            Text(
              'cipherTextValues : $cipherTextValues',
            ),

            // 復号する
            ElevatedButton(
              onPressed: () async{
                
                // 復号
                final List<int> decryptedValues = await algorithm.decrypt(
                  secretBox,
                  secretKey: secretKey,
                );

                setState(() {
                  // List<int>をStringに変換
                  decryptedText = utf8.decode(decryptedValues);
                });
              }, 
              child: const Text('decrypt'),
            ),

            Text('decryptedText(List<int>) = ${utf8.encode(decryptedText)}'),
            Text('decryptedText = $decryptedText'),
          ],
        ),
      ),
    );
  }
}



実行画面

入力後

入力後

「decrypt」を押した後

decryptを押した後