SharedPreferencesに保存する文字列を暗号化

こんにちは。

MODE_PRIVATEに設定したSharedPreferencesは、自アプリのみ読み書き可能となります。
でもここでひとつ、SharedPreferencesに保存する文字列を暗号化しておこうではありませんか。

ということで当記事は、単に「AndroidのSharedPreferencesに、暗号化した文字列を格納する（し、復号もしたい）」という目的で、ヤッテミタ、というものです。
暗号化そのものについては、こちらの記事「[プログラマの暗号化入門](https://qiita.com/asksaito/items/1793b8d8b3069b0b8d68)」が分かりやすいです。

開発環境は以下の通りです。

> Android Studio 3.0.1
> Build #AI-171.4443003, built on November 10, 2017
> JRE: 1.8.0_152-release-915-b01 amd64
> JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
> Windows 10 10.0

# レイアウトリソース

TextView１個だけの画面です。

```xml:/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="jp.co.casareal.securesharedpreferences.MainActivity">

    <TextView
        android:id="@+id/beginning"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>
```

# 暗号化/復号のユーティリティ

```java:CryptUtil.java
package jp.co.casareal.securesharedpreferences.util;

import android.util.Base64;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * 暗号化/復号化のユーティリティ
 */
public class CryptUtil {
    // アルゴリズム
    private static final String ALGORITHM = "Blowfish";
    private static final String MODE = "Blowfish/CBC/PKCS5Padding";
    private static final String IV = "abcdefgh"; // Blowfishの場合は64ビット（8バイト）なので

    /**
     * 引数の文字列を暗号化する(Base64対応)
     *
     * @param value     暗号化対象文字列
     * @param secretKey 暗号化キー
     * @return String 暗号化済み文字列
     */
    public String encrypt(String value, String secretKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), ALGORITHM);
        Cipher cipher = Cipher.getInstance(MODE);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(IV.getBytes()));
        byte[] values = cipher.doFinal(value.getBytes());
        return Base64.encodeToString(values, Base64.DEFAULT);
    }

    /**
     * 引数のBase64された文字列を復号化する
     *
     * @param value     復号化対象文字列
     * @param secretKey 復号化キー
     * @return String 復号化済み文字列
     */
    public String decrypt(String value, String secretKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        byte[] values = Base64.decode(value, Base64.DEFAULT);
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), ALGORITHM);
        Cipher cipher = Cipher.getInstance(MODE);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(IV.getBytes()));
        return new String(cipher.doFinal(values));
    }
}
```

ほとんどJava SE APIのコードですね。`android.util.Base64`だけがAndroidのAPIです。
それにしても、暗号化に関するAPIの例外の種類の多いこと多いこと。

# Activity

```java:MainActivity.java
package jp.co.casareal.securesharedpreferences;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;

import jp.co.casareal.securesharedpreferences.util.CryptUtil;

public class MainActivity extends AppCompatActivity {
   // ログ用タグ
    private static final String TAG = "MainActivity";

    // 暗号化/復号キー
    private static final String secretKey = "Le vent se lève, il faut tenter de vivre.";

    private SharedPreferences sharedPreferences;
    private String sharedPreferencesName = "kazetachinu";

    // 堀辰雄 著「風立ちぬ」冒頭
    private String beginning = "それらの夏の日々、一面に薄の生い茂った草原の中で、お前が立ったまま熱心に絵を描いていると、私はいつもその傍らの一本の白樺の木蔭に身を横たえていたものだった。"; // 「薄」は、植物のススキです。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sharedPreferences = getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE);
    }

    @Override
    protected void onStart() {
        super.onStart();
        try {
            saveString(secretKey, beginning); // 暗号化
        } catch (Exception e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        try {
            String beginning = getString(secretKey); // 復号
            ((TextView) findViewById(R.id.beginning)).setText(beginning);
        } catch (Exception e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    /**
     * 文字列保存
     *
     * @param key   キー
     * @param value 値
     * @throws Exception 例外
     */
    private void saveString(String key, String value) throws Exception {
        if (key == null || key.length() == 0) {
            throw new Exception("キーが空です。");
        }
        if (value == null) {
            throw new Exception("値が空です。");
        }
        // 暗号化
        CryptUtil cryptUtil = new CryptUtil();
        String encValue = cryptUtil.encrypt(value, secretKey);
        if (encValue == null) {
            throw new Exception("暗号化に失敗しました。");
        }

        // 保存
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(key, encValue).commit();
    }

    /**
     * 文字列取得
     *
     * @param key キー
     * @return String キーに紐づいた値(無ければnullを返す)
     * @throws Exception 例外
     */
    private String getString(String key) throws Exception {
        // 値取得
        if (key == null || key.length() == 0) {
            throw new Exception("キーが空です。");
        }
        String value = sharedPreferences.getString(key, null);
        if (value == null) {
            return null;
        }

        // 復号
        CryptUtil cryptUtil = new CryptUtil();
        String decValue = cryptUtil.decrypt(value, secretKey);
        if (decValue == null) {
            throw new Exception("復号に失敗しました。");
        }
        return decValue;
    }
}
```

1. SharedPreferencesに、暗号化した文字列を書き込んで、
2. 読み込んで、
3. 画面上のTextViewに表示する。

というだけのアプリです。

# SharedPreferencesファイルを見てみます

```xml:kazetachinu.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="Le vent se lève, il faut tenter de vivre.">w8elOyVO0SCF7AMWoI8Is7I01wIDRYraSxpMaOJ8L8mmwTtlW18X0fh6rv+9puDlI6OmRvMCe/K0
wRgNmFsKBydof+y+ktKIcoNeWtW2koNF1TSOPmji3sDyJlkYURq1T/LOF3me1YR1IhCPU72NcWts
505he9EanQ1hjnerHz6GM8zEEKTJS/UIT/sj+da0v4n2czW1oOBg0UXg36dzqhtZhwwmYTpu2UyK
W//iT3fvgRQWm1GCjL1lMv2cQgZiOkN+nzDZAQ3Ifpg7cen3jsGNQ/SfWKJnMtEs8+fmqwN0rAvI
K7XSSaIcbHouZrko
</string>
</map>
```

私はこれまで書いて来たシェアドプリファレンスをすっかり読みかえして見た。私の意図したところは、これならまあどうやら自分を満足させる程度には書けているように思えた。

<ruby>風立ちぬ、いざ生きめやも。<rt>Le vent se lève, il faut tenter de vivre.</rt></ruby>