Realmの暗号化とAndroid System

69
C-LIS CO., LTD.

Transcript of Realmの暗号化とAndroid System

C-LIS CO., LTD.

自己紹介

Android Studio本

3

2014年11月21日発売

技術評論社刊

Android Studio 0.8.6http://amzn.to/1HYRp32

4

5

https://github.com/keiji/the-androidstudio-book

コミックマーケット87 Android Studioセットアップガイド

6

https://anharu.keiji.io

はじめての Androidアプリ開発レッスン

7

改訂版が出ます(2016年1月上旬予定)

改訂版の原稿を送ったのが11月16日

9

11月20日

サンプルコードのプロジェクトを

Android 1.5用に調整したのが11月23日

11月24日

11

Android Studio 2.0 Preview

続く

Realmの暗号化と

Android System

2015/11/25 Realm Meetup #9

今日の内容

データの保護

15

 ユーザーや悪意のある開発者からデータを保護したい。

 安全にデータを保管するための方法として「javax.cryptoによるデータベースファイルの保護」「SQLCipherを使った保護」「Realmの暗号化機能」 の、三つについて、それぞれ利用して比較する。

鍵の保護

16

 暗号化したデータの復号鍵をどこに保存するかは大きな課題である。

 Android 6.0で大幅に強化された「Android Keystore System」を使って、データを安全に保護する方策を検討する。

 

データの保護を検討する

https://github.com/keiji/realm_meetup_9/releases/tag/realm_meetup_9

{ "characters": [ { "name": "Uduki Shimamura", "age": "17", "megane": false }, { "name": "Rin Shibuya", "age": "15", "megane": false }, { "name": "Haruna Kamijo", "age": "18", "megane": true },

サンプルアプリ

18

https://github.com/keiji/realm_meetup_9/releases/tag/realm_meetup_9

アプリの動作

https://github.com/keiji/realm_meetup_9/releases/tag/realm_meetup_9

{ "characters": [ { "name": "Uduki Shimamura", "age": "17", "megane": false }, { "name": "Rin Shibuya", "age": "15", "megane": false }, { "name": "Mio Honda", "age": "15", "megane": false }, { "name": "Hina Araki", "age": "20", "megane": false },

characters.json

Database

ユーザー

検索 & 一覧表示

JSONパース 保存

19

検索条件{ "characters": [ { "name": "Uduki Shimamura", "age": "17", "megane": false }, { "name": "Rin Shibuya", "age": "15", "megane": false }, { "name": "Haruna Kamijo", "age": "18", "megane": true }, これがtrue

https://github.com/keiji/realm_meetup_9/releases/tag/realm_meetup_920

実行画面

21

javax.crypto

22

•Androidに組み込まれている暗号化フレームワーク

•AES 256-bitに対応している

暗号化したデータベースファイルを、

利用時に限定して復号する

@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user_list); mListView = (ListView) findViewById(R.id.listview); getSupportLoaderManager().restartLoader(LOADER_ID_DECRYPT, null, mCipherLoaderCallback); } @Overrideprotected void onDestroy() { super.onDestroy(); mDb.close(); new EncryptTask(getDatabasePath(DbHelper.DB_FILE_NAME)).execute();}

sqlite.SQLiteActivity

23

private static final String TRANSFORMATION = "AES/CBC/PKCS7Padding"; private static final byte[] KEY = "thisismypa55w0rdthisismypa55w0rd".getBytes();

sqlite.SQLiteActivity

24

SecretKeySpec key = new SecretKeySpec(KEY, "AES"); Cipher cipher = Cipher.getInstance(TRANSFORMATION);cipher.init(Cipher.ENCRYPT_MODE, key);byte[] buffer = new byte[cipher.getBlockSize()];int len;while ((len = is.read(buffer)) != -1) { byte[] encrypted = cipher.update(buffer, 0, len); os.write(encrypted);} os.write(cipher.doFinal());rawDbFile.deleteOnExit();

32byte必要

テーブル構造

25

private static final String CREATE_TABLE_USERS = "CREATE TABLE characters ( " + "_id INTEGER PRIMARY KEY," + "name TEXT," + "age INTEGER," + "megane INTEGER" + " )";

public static Character read(Cursor cursor) { Character user = new Character(); user.name = cursor.getString(cursor.getColumnIndex("name")); user.age = cursor.getInt(cursor.getColumnIndex("age")); user.megane = cursor.getInt(cursor.getColumnIndex("megane")) == 1; return user;} public static Cursor findAllMeganeCursor(SQLiteDatabase db) { return db.query("characters", new String[]{"_id", "name", "age", "megane"}, "megane = ?", new String[]{Integer.toString(1)}, null, null, null); }

sqlite.DbHelper.java

sqlite.entity.Character.java

問題点

26

復号してデータベースにアクセスしている間、暗号化されていないデータがファイルシステム上に存在する

SQLCipher

27

•Zetetic社が開発、提供する暗号化機能付きSQLite

•AES 256-bitで暗号化

•透過的にアクセス可能

暗号化したままデータを読み書きする

公式ではバイナリを配布せず

28

SQLCipherは、基本的に有償版を推しているイメージ。

オープンソース版のCommunity Editionは個々でビルドする

dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.1.1' compile 'net.zetetic:android-database-sqlcipher:3.3.1-2'}

BintrayでAndroid版を入手

app/build.gradle

29

private static final String DB_PASSWORD = "pa55w0rd";

@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SQLiteDatabase.loadLibs(this); setContentView(R.layout.user_list); mListView = (ListView) findViewById(R.id.listview); if (!getDatabasePath(DbHelper.DB_FILE_NAME).exists()) { mDb = new DbHelper(this).getWritableDatabase(DB_PASSWORD); getSupportLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallback);

sqlcipher.SQLCipherActivity

30

テーブル構造

31

private static final String CREATE_TABLE_USERS = "CREATE TABLE characters ( " + "_id INTEGER PRIMARY KEY," + "name TEXT," + "age INTEGER," + "megane INTEGER" + " )";

public static Character read(Cursor cursor) { Character user = new Character(); user.name = cursor.getString(cursor.getColumnIndex("name")); user.age = cursor.getInt(cursor.getColumnIndex("age")); user.megane = cursor.getInt(cursor.getColumnIndex("megane")) == 1; return user;} public static Cursor findAllMeganeCursor(SQLiteDatabase db) { return db.query("characters", new String[]{"_id", "name", "age", "megane"}, "megane = ?", new String[]{Integer.toString(1)}, null, null, null); }

sqlcipher.DbHelper.java

sqlcipher.entity.Character.java

java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[

DexPathList[

[zip file "/data/app/io.keiji.realmsample2-2/base.apk"],

nativeLibraryDirectories=[/data/app/io.keiji.realmsample2-2/lib/arm64,

/data/app/io.keiji.realmsample2-2/base.apk!/lib/arm64-v8a, /vendor/lib64, /system/lib64]]]

couldn't find "libstlport_shared.so"

at java.lang.Runtime.loadLibrary(Runtime.java:367)

at java.lang.System.loadLibrary(System.java:1076)

at net.sqlcipher.database.SQLiteDatabase.loadLibs(SQLiteDatabase.java:173)

at net.sqlcipher.database.SQLiteDatabase.loadLibs(SQLiteDatabase.java:169)

at io.keiji.realmsample2.sqlcipher.SQLCipherActivity.onCreate(SQLCipherActivity.java:83)

at android.app.Activity.performCreate(Activity.java:6237)

Android 6.0(Marshmallow)

32

問題点

33

SQLCipherのSQLiteDatabaseクラスは、 net.sqlcipher.databaseパッケージに所属している。SQLiteDatabaseとの継承関係もないため、一般的なORMとの相性は良くないと思われる

Realm

34

•Realm社が開発、提供するNoSQLデータベース

•AES 256-bitで暗号化

•透過的にアクセス可能

読み書き中もデータは暗号化されたまま

データが抜き出されても現実的な時間では解読が困難

public class RealmAdapter { private static final byte[] KEY = "thisismypa55w0rdthisismypa55w0rdthisismypa55w0rdthisismypa55w0rd".getBytes(); public RealmConfiguration getRealmConfiguration(Context context) { return new RealmConfiguration.Builder(context) .encryptionKey(KEY) .build(); }} 64byte必要

@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user_list); mRealmAdapter = new RealmAdapter(); mRealm = Realm.getInstance(mRealmAdapter.getRealmConfiguration(this));

realm.RealmActivity

35

RealmResults<Character> realmResult = mRealm .where(Character.class) .equalTo("megane", true) .findAll();

mListView = (ListView) findViewById(R.id.listview); mAdapter = new Adapter(this, realmResult, true); mListView.setAdapter(mAdapter);

情報を検索して表示

public class Adapter extends RealmBaseAdapter<Character> { @Override public View getView(int position, View convertView, ViewGroup parent) { Character character = getItem(position); ViewHolder holder = null; if (convertView == null) { convertView = View.inflate(RealmActivity.this, R.layout.user_list_row, null); holder = new ViewHolder((TextView) convertView.findViewById(R.id.label)); convertView.setTag(holder); }

36

遭遇した課題

createObjectFromJson

38

{ "characters": [ { "name": "Uduki Shimamura", "age": "17", "megane": false }, { "name": "Rin Shibuya", "age": "15", "megane": false }, { "name": "Haruna Kamijo", "age": "18", "megane": true },

public class Character extends RealmObject { private long id; private String name; private Integer age; private boolean megane = true;

public class Characters extends RealmObject { private RealmList<Character> characters;

createObjectFromJson

39

public class JsonLoaderRealmJson extends AsyncTaskLoader<LoaderResult> {

@Overridepublic LoaderResult loadInBackground() { LoaderResult result; Realm realm = Realm.getInstance(realmAdapter.getRealmConfiguration(getContext())); try { realm.beginTransaction(); realm.createObjectFromJson(Characters.class, getContext().getAssets().open(JSON_FILE_NAME)); realm.commitTransaction(); result = new LoaderResult(null); } catch (IOException e) {

想定していないデータの存在

40

{ "name": "Kirari Moroboshi", "age": "17", "megane": false }, { "name": "Nana Abe", "age": "永遠の17歳", "megane": false }, { "name": "Akiha Ikebukuro", "age": "14", "megane": true } ]

public class Character extends RealmObject { private long id; private String name; private Integer age; private boolean megane = true;

public class Characters extends RealmObject { private RealmList<Character> characters;

課題の解決 - JPP

41

JsonPullParserhttps://github.com/vvakame/JsonPullParserを導入して、Jsonのパース処理をRealmから切り離した。

参考: RealmとJSONライブラリ by zaki50 https://speakerdeck.com/zaki50/realmtojsonraiburari

課題の解決

42

@JsonModelpublic class Characters4Jpp { @JsonKey private List<Character> characters; public List<Character> getCharacters() { return characters; } public void setCharacters(List<Character> characters) { this.characters = characters; }}

converterを指定

43

@JsonModelpublic class Character extends RealmObject { @JsonKey private long id; @JsonKey private String name; @JsonKey(converter = AgeTokenConverter.class) private Integer age; @JsonKey private boolean megane = true;

converter

44

public class AgeTokenConverter extends TokenConverter<Integer> { static AgeTokenConverter converter = null; public static AgeTokenConverter getInstance() { if (converter == null) converter = new AgeTokenConverter(); return converter; } @Override public Integer parse(JsonPullParser parser, OnJsonObjectAddListener listener) throws IOException, JsonFormatException { Integer value = null; if (parser.getEventType() == JsonPullParser.State.VALUE_STRING) { String str = parser.getValueString(); try { value = !TextUtils.isEmpty(str) ? Integer.parseInt(str) : null; } catch (NumberFormatException e) { } } return value;

鍵の保護を検討する

鍵をどこに置く?

46

private static final String DB_PASSWORD = "pa55w0rd";

private static final byte[] KEY = "thisismypa55w0rdthisismypa55w0rdthisismypa55w0rdthisismypa55w0rd".getBytes();

private static final byte[] KEY = "thisismypa55w0rdthisismypa55w0rd".getBytes();

Android Keystore System

47

 Android端末のセキュリティ・ハードウェア(Secure Element, Trusted Execution Environment)内に鍵情報を格納する。

 Android 4.3から導入されていたがAndroid 6.0(Marshmallow)でAES共通鍵に対応するなど機能が強化された。

http://developer.android.com/intl/ja/training/articles/keystore.html

Android Keystore System

48

 javax.cryptoと密接に連携している。

 任意の情報を保存できるわけではない(=Realmの暗号化キーを直接保存できない)。

http://developer.android.com/intl/ja/training/articles/keystore.html

Android Keystore System

49

Realm暗号キー 64bytes

Android Keystore System

鍵情報 AES

暗号キー 64bytes

暗号化済

ユーザー認証

Storage

http://developer.android.com/intl/ja/training/articles/keystore.html

realm_with_ask.RealmActivity

50

static final String PROVIDER_NAME = "AndroidKeyStore"; static final String KEY_ALIAS = "auth_key";

private static void generateKey() { try { KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, PROVIDER_NAME); keyGenerator.init(new KeyGenParameterSpec.Builder( KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) .setUserAuthenticationValidityDurationSeconds( AUTH_VALID_DURATION_IN_SECOND) .build()); keyGenerator.generateKey(); } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException

realm_with_ask.RealmActivity

51

void authorize() { KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); Intent intent = km.createConfirmDeviceCredentialIntent("Android Keystore System", "Android Keystore Systemに保存した鍵を使ってRealmのデータベースにアクセスします"); startActivityForResult(intent, REQUEST_CREDENTIAL);}

realm_with_ask.RealmActivity

52

realm_with_ask.RealmActivity

53

void readyRealm() { try { Cipher cipher = Cipher.getInstance(TRANSFORMATION); byte[] encryptedKey = null; File file = new File(getFilesDir(), KEY_FILE_NAME);

// 暗号化された鍵情報の読み込み → encryptedKey RealmAdapter.setKey(decryptRealmKey(encryptedKey, cipher));

realm_with_ask.RealmActivity

54

byte[] encryptRealmKey(byte[] passkey, Cipher cipher) { try { SecretKey key = (SecretKey) mKeyStore.getKey(KEY_ALIAS, null); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] encrypted = cipher.doFinal(passkey); byte[] initializationVector = cipher.getIV(); byte[] result = new byte[initializationVector.length + encrypted.length]; System.arraycopy(initializationVector, 0, result, 0, initializationVector.length); System.arraycopy(encrypted, 0, result, initializationVector.length, encrypted.length); return result;

Initialization Vector

16 bytes

encryption key

64 bytes

realm_with_ask.RealmActivity

55

byte[] decryptRealmKey(byte[] encrypted, Cipher cipher) { byte[] initializationVector = new byte[16]; byte[] passKey = new byte[encrypted.length - initializationVector.length]; System.arraycopy(encrypted, 0, initializationVector, 0, initializationVector.length); System.arraycopy(encrypted, initializationVector.length, passKey, 0, passKey.length); try { SecretKey key = (SecretKey) mKeyStore.getKey(KEY_ALIAS, null); IvParameterSpec ivSpec = new IvParameterSpec(initializationVector); cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); return cipher.doFinal(passKey);

Fingerprint API

56

 Android 6.0(Marshmallow)で追加。

 Android Keystore Systemをパターンロック・パスワードに加えて指紋スキャナーで認証する。

http://developer.android.com/intl/ja/about/versions/marshmallow/android-6.0.html#fingerprint-authentication

RealmFingerprintActivity

57

private final AuthenticationCallback mAuthenticationCallback = new FingerprintManager.AuthenticationCallback() {

@Override public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { super.onAuthenticationSucceeded(result); readyRealm(); }};

RealmFingerprintActivity

58

@Override void authorize() { FingerprintManager fingerprintManager = (FingerprintManager) getSystemService(FINGERPRINT_SERVICE); try { SecretKey key = (SecretKey) mKeyStore.getKey(KEY_ALIAS, null); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, key); FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher); Toast.makeText(this, "指紋スキャナーに指を当てて下さい...", Toast.LENGTH_LONG) .show(); //noinspection ResourceType fingerprintManager.authenticate(cryptoObject, null, 0, mAuthenticationCallback, null);

RealmFingerprintActivity

59

今日の内容 - まとめ

今日の内容 - まとめ

61

 安全にデータを保管するための方法として「javax.cryptoによるデータベースファイルの保護」「SQLCipherを使った保護」「Realmの暗号化機能」

 の3パターンを、それぞれ比較した。

今日の内容 - まとめ

62

 「javax.cryptoによるデータベースファイルの保護」では、SQLDatabaseのファイルを必要なときだけ復号した。

 しかし、読み書きの最中はファイルシステム上に復号したデータファイルが存在するため、安全とは言えなかった。

今日の内容 - まとめ

63

 「SQLCipherを使った保護」では、SQLDatabaseのファイルを暗号化した上で全体を復号することなく、透過的に読み書きができた。

 しかし、Android 6.0で正常な動作が確認できなかった。また、そのままではActiveAndroidなどORMを適用しにくいという課題もあった。

今日の内容 - まとめ

64

 Realmを使うと、データベースを暗号で保護した状態で、透過的に扱うことができた。

 JSONからモデルに変換する機能は、異常値が含まれる場合などは対応しきれないケースは、JsonPullParserのconverterを使って解決した。

 

今日の内容 - まとめ

65

 「Android Keystore System」を使うことで、機密データの復号鍵をセキュリティ・ハードウェアに保護された領域に保存して、安全に格納することができる。

 ただし、Realmはjavax.crypto.Cipherを直接扱えないため、暗号化に用いるキーをAndroid Keystore Systemを用いて暗号化。キーをファイルシステムに保存した。

C-LIS CO., LTD.

各製品名・ブランド名、会社名などは、一般に各社の商標または登録商標です。本資料中では、©、®、™を割愛しています。

本資料は、有限会社シーリスの著作物です。掲載されているイラストは、特に記載がない場合は根雪れいの著作物です。

本資料の全部、または一部について、著作者から文書による許諾を得ずに複製することは禁じられています。

Material Icons are reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 4.0 Attribution license.

http://techbooster.github.io/c89/

Copyright TechBooster

C89 - TechBooster 3日目東シ58a

Copyright TechBooster

C89 - TechBooster 3日目東シ58a

Copyright TechBooster