
モビリティ
イベント
該当するコンテンツが見つかりませんでした
マガジン
技術ブログ
SDV(Software-Defined Vehicle)時代における自動車の品質保証として、サイバーセキュリティ監査の重要性を解説します。国際法規(UN-R155/156)で義務化された背景、プロセス監査と製品監査で確認される内容、基盤となるISO/SAE 21434などの規格、推奨される資格について紹介。TARA(脅威分析)を起点とし、セキュリティ要求を設計段階から組み込むなど、監査に耐えうる製品開発の5つの重要な観点を説明します。
こんにちは。KINTOテクノロジーズ(KTC)でKINTOの中古車ECサイトのディレクターをしている かーびー です。KINTO Technologiesでは 「ユーザーファースト」 を会社の重点方針のひとつに掲げ、全社でさまざまな取り組みが進んでいます。私も自分のチームで、ユーザーインタビューの録画をみんなで見る 「ユーザーインタビューわいわい会」 を試すなど、お客様の一次情報に触れる場づくりに取り組んできました。 こうした取り組みをきっかけに、現在はユーザーファーストを社内に広めるための活動にも運営メンバーのひとりとして関わっています。そのひとつが、今回ご紹介する社内勉強会「ユーザーに寄りそわNight! Vol.02」です。 自分たちのサービスを、ユーザーが使っているところを見たことはありますか? 勉強会の中で参加者にこの質問をしたところ、約7割が「ない」と回答しました。 関心がないのではなく、日常の開発フローの中にその機会がない。要件をヒアリングして、仕様に落とし込んで、品質の高いものを作って届ける。エンドユーザーがどんなふうにサービスを使っているかに触れる機会は、意外と少ないのが現実です。 しかもKINTOテクノロジーズの場合、関わるサービスはトヨタ自動車、株式会社KINTO、開発を担う私たちなど、複数の組織で成り立っています。本来なら1社の中で完結する「作って、使ってもらって、フィードバックをもとに改良する」という流れを、組織をまたいで回していく。ここが私たちの組織ならではの難しさだなと感じています。 関わる人が増えるほど、それぞれの立場や見えている景色は違ってきます。だからこそ、作っている一人ひとりがユーザーの姿を知っていることが大事になる。「あのお客様、こう言っていたよね」という共通の記憶がチームにあると、議論もかみ合いやすくなります。 言われたものを作るだけじゃなく、自分たちから価値を届けていく。「ユーザーに寄りそわNight!」は、ユーザーを知るために踏み出した社内チームの取り組みを紹介する勉強会です。 方法論の講義ではなく、隣のチームの体験を共有する場 この勉強会で大事にしているのは、 「私にもできそう!」 と思えることです。 ユーザーリサーチの手法を網羅的に学ぶ場ではなく、他のチームの取り組みを聞いて「これなら自分のチームでもできそう」と感じてもらう。そんな場でありたいと考えています。 toCでもtoBでも、自分たちの仕事の先には必ず使う人がいます。その誰かに寄りそっていくことが、ユーザーファーストの根っこにある考え方だと捉えています。 こうした考えから、勉強会では実際にユーザーと向き合う取り組みをしたチームに登壇してもらい、何をやって、何に気づいたかを共有してもらう形式にしています。専門的な方法論の紹介ではなく、隣のチームの体験を聞くこと。そこから自分のチームでも試してみたいと思える、小さなきっかけが生まれる場になればと思っています。 ユーザーに寄りそわNight! Vol.02:ユーザーと同じ環境で、プロダクトを使ってみる 2026年3月に開催された第2回の勉強会では、実際にユーザーが使っているのと同じような環境で、自分たちもプロダクトをテストしてみるーーそんな取り組みをしているチームに登壇してもらいました。ユーザーファーストの取り組みとして、社内の各所で生まれている実践をキャッチして勉強会に繋げていく中で、この取り組みのことを知り、声をかけたのが始まりでした。 トヨタグループには「現地現物」——実際の現場に足を運び、自分の目で見て判断する——という考え方があります。登壇してくれたチームはこの考え方をユーザー理解にも活かしたいと、開発メンバー自身がユーザーと同じ状況に身を置いてプロダクトを使ってみる、という取り組みに挑戦していました。 机の前の3秒、現場の3秒 登壇でとくに印象に残ったのは、開発環境ではわからなかったことが、ユーザーと同じ状況で使ってみると次々に見えてきたという話でした。 たとえばアプリの表示にかかる時間。開発環境で3秒かかっても「ちょっと遅いな」と感じる程度だけれど、ユーザーが実際に使う状況で体験する3秒はまるで別物。急いでいるとき、周りに人がいるとき、落ち着いて待てないとき。クーラーの効いたオフィスで感じる3秒と、現場で感じる3秒は、同じ時間とは思えないくらい違って感じられた、と。 「仕様通りに動く」はずのものが、ユーザーと同じ状況に置かれるとまったく違う顔を見せる。データでは見えない課題が、身体で感じられる瞬間でした。 「忖度を捨てる」という第一歩 では、現場で気づいたことをどう日常の開発に持ち帰っていくか。パネルディスカッションで印象に残ったのは、「忖度を捨てる」という言葉でした。 「アプリを使っていて『ここ遅いな』と思っても、『APIをたくさん呼んでるからしょうがないか』と開発者としての忖度をしてしまう。その忖度をあえて捨てて、純粋にユーザーとしてアプリを使ってみることが、まずできる第一歩」 開発者として「これはしょうがないか」と自分で飲み込んでしまう場面は、きっと多くの人に心当たりがあると思います。その忖度を一度横に置いて、純粋にユーザーとしてアプリを触ってみる。大がかりな準備をしなくても、今日から始められる小さな一歩として、とても印象に残った言葉でした。 これからも、小さな一歩を重ねていく Vol.02の懇親会では、「うちのチームでもこういうことをやってみたい、でもどう始めればいいんだろう?」という声や、登壇者を囲んで「どうやって社内を巻き込んでいったんですか?」と具体的な進め方を聞く姿が、あちこちで見られました。 アンケートのフリーコメント欄には、約半数の方が「これから自分のチームでやってみたいこと」を書き込んでくれました。印象的だったのは、toCのサービスを作っているチームだけでなく、業務システムやプラットフォームを担当する方々からも、具体的な一歩の言葉が並んだことです。 「業務システムなのでユーザーがKINTO社員であり距離が近い。実際に業務をやらせてもらったり、フィードバックを貯める場を作ったりして、ユーザーファーストを実践する場を作りたい」 「忖度せずに改善アイデアを出し、検討する。アイデアを歓迎する空気を作っていきたい」 自分たちの仕事の先にいる「使う人」は、toCのお客様だけではありません。社内の誰か、パートナー企業の誰か、ときには自分自身かもしれない。それぞれの現場で、それぞれの「寄り添い方」がある。そのことを、登壇してくれたチームの話と、参加者の声から改めて感じた回でした。 Vol.01の開催から半年、社内Slackチャンネルのメンバーは60人から99人に増え、「うちでもこういうことやってるよ!」と声をかけてくれる人も出てきています。これまで各チームの中に閉じていた取り組みが、少しずつ表に出てくるようになりました。 「ユーザーファースト」は2025年の注力テーマとして始まりましたが、ユーザーのことを考えるのはプロダクト開発の基礎の基礎。一年限りのテーマで終わらせず、Vol.03に向けた準備も進行中です。 大がかりな取り組みでなくても、まずは自分のプロダクトをユーザーとして使ってみることから。気づいたことを隣の人に話してみることから。一つひとつのチームで生まれる小さな一歩を、勉強会という場で共有し、また次の一歩へつなげていく。この取り組みの火を絶やさないよう、これからも続けていきます。
はじめに KINTOテクノロジーズの大沼です。 モビリティサービス「my route」アプリの開発に従事しています。 本記事では、AndroidのKeystore、Cipher、DataStoreを使用して秘匿情報の暗号化と永続化を実装した際の実装詳細とハマった点・注意点をまとめました。 こちら大杉さんの記事 では、Tink を使用したケースとパフォーマンス検証を紹介しているのでぜひご一読ください。 💬 実装の前にディスカッション 🔍 本当に暗号化が必要なのか DroidKaigi 2025のyanzamさんのお話 でも触れられてましたが、Keystore がクラッシュするのは黙認しつつ最低限の機会頻度に抑えたいので、既存で暗号化しているデータが本当に暗号化する必要があるのかの議論をチームメンバーと交わしました。 案の定、不要に暗号化しているものもあり、議論することで最適なものに絞ることができました。 🏗️ アーキテクチャ セキュリティに関するリファクタリングのコードレビューは、心理負荷が高いと考えています。 私のチームはこういう時、大枠の実装方針を事前に共有し合うことで、コードレビュー時の認識違いや負担が減らせます。 今回、データの暗号化とインターフェースを以下のようなスライドで共有し、大きな齟齬なくレビューを進めることができました。 🛠️ 実装の流れ ここからは、実際の実装手順を以下の流れで解説します。 依存関係の追加 — DataStoreライブラリの導入 Keystoreを使った暗号化キーの生成 — AES/GCMの鍵をAndroid Keystoreで安全に管理 Cipherを使った暗号化・復号化 — 初期化ベクトル(IV)の扱いを含む暗号処理の実装 DataStoreへの保存 — 暗号化したデータをPreferences DataStoreで永続化し、Flowで読み出す 📚 依存関係の追加 ライブラリにDataStoreを追加します。 dependencies { // DataStore implementation("androidx.datastore:datastore-preferences:1.1.7") } 🔑 Keystoreを使った暗号化キーの生成 import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import java.security.KeyStore import javax.crypto.KeyGenerator ... fun getOrCreateSecretKey(): SecretKey? { try { // KeyStoreのインスタンス生成 val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER).apply { load(null) // KeyStoreを初期化するための必須の呼び出し } // KeyStoreにプロダクトの鍵が存在するか確認し、あれば取得し返す if (keyStore.containsAlias(PROJECT_KEY_STORE_ALIAS)) { val entry = keyStore.getEntry(PROJECT_KEY_STORE_ALIAS, null) if (entry is KeyStore.SecretKeyEntry) { return entry.secretKey } } // KeyStoreにプロダクトの鍵が存在しなければ生成して保存し返す val params = KeyGenParameterSpec.Builder( PROJECT_KEY_STORE_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(256) .build() val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER, ) keyGenerator.init(params) return keyGenerator.generateKey() } catch (e: Exception) { Firebase.crashlytics.recordException(e) return null } } 🔐 Cipherを使った暗号化・復号化 import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec interface CryptographyManager { fun encrypt(plaintext: String): String fun decrypt(encryptedString: String): String } private const val TRANSFORMATION = "AES/GCM/NoPadding" private const val IV_SIZE_BYTES = 12 private const val TAG_SIZE_BITS = 128 class CryptographyManagerImpl : CryptographyManager { override fun encrypt(plaintext: String): String { return try { val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey()) val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8)) val ivAndCiphertext = cipher.iv + ciphertext // IVと暗号文をバイト配列として結合 Base64.getEncoder().encodeToString(ivAndCiphertext) } catch (e: Exception) { try { CryptographyException.parse(e) } catch (cryptoException: CryptographyException) { Firebase.crashlytics.recordException(cryptoException) "" } } } override fun decrypt(encryptedString: String): String { return try { val cipher = Cipher.getInstance(TRANSFORMATION) val ivAndCiphertext = Base64.getDecoder().decode(encryptedString) // 復号化時に保存したIVを使う val spec = GCMParameterSpec(TAG_SIZE_BITS, ivAndCiphertext, 0, IV_SIZE_BYTES) cipher.init(Cipher.DECRYPT_MODE, getOrCreateSecretKey(), spec) val plaintext = cipher.doFinal( ivAndCiphertext, IV_SIZE_BYTES, ivAndCiphertext.size - IV_SIZE_BYTES, ) String(plaintext, Charsets.UTF_8) } catch (e: Exception) { try { CryptographyException.parse(e) } catch (cryptoException: CryptographyException) { Firebase.crashlytics.recordException(cryptoException) "" } } } } 💾 DataStoreへの保存 import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map data class SecureDataPreferences( val textData: String, ) object PreferencesKeys { private val TEXT_KEY = stringPreferencesKey("encrypted_text") } private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "encrypted_prefs") class SecureDataRepository( private val cryptographyManager: CryptographyManager ) { suspend fun saveTextData(data: String) { val encryptedData = cryptographyManager.encrypt(data) dataStore.edit { preferences -> preferences[TEXT_KEY] = encryptedData } } private val secureDataFlow: Flow<SecureDataPreferences> = secureDataStore.data .catch { exception -> if (exception is IOException) { emit(emptyPreferences()) } else { throw exception } } .map { it.mapSecureDataPreferences() } private fun Preferences.mapSecureDataPreferences(): SecureDataPreferences { return SecureDataPreferences( textData = this[PreferencesKeys.TEXT_KEY]?.let { cryptographyManager.decrypt(it) } ?: "", // ... Other data ) } suspend fun getTextData(): String { return try { withTimeout(3000L) { secureDataFlow.map { it.textData }.first { it.isNotBlank() } } } catch (_: TimeoutCancellationException) { "" } catch (_: NoSuchElementException) { "" } } } ⚠️ ハマった点・注意点 1. 初期化ベクトル(IV)の保存 暗号化時に生成されるIV(Initialization Vector)は、復号化時に必須です。 IVは暗号文と一緒に保存する必要があります。IVは秘密情報ではないため、平文で保存しても問題ありません。 ハマったポイント: 最初の実装でIVを保存し忘れ、復号化時に javax.crypto.AEADBadTagException が発生しました。 2. KeyStoreのキーのライフサイクル Android Keystoreに保存されたキーは、アプリをアンインストールすると削除されます。 また、デバイスのロック画面が解除されるまでキーにアクセスできない設定も可能です( setUserAuthenticationRequired(true) )。 注意点: keyが存在しない場合の処理を適切に実装する必要があります。 3. GCMモードのタグ長 GCM(Galois/Counter Mode)を使用する場合、タグ長を正しく設定する必要があります。 一般的には128ビット(16バイト)が使用されます。 4. エラーハンドリング 復号化時にはさまざまなエラーが発生する可能性があります: KeyPermanentlyInvalidatedException : キーが無効化された AEADBadTagException : 暗号文が改ざんされた、またはIVが間違っている InvalidKeyException : キーが無効 これらのエラーをハンドリングし、必要に応じてデータをクリアし再生成するなどの対応が必要です。 5. DataStoreの非同期処理 DataStoreはすべての操作が非同期で行われます。 CoroutineまたはFlowを使用して適切に処理する必要があります。 DataStoreのソースコード内で、最新の値を1つだけ取得できる data.first() を使用することを推奨しています。 // ViewModelでの使用例 viewModelScope.launch { repository.saveTextData(sensitiveData) } // Flowの監視 repository.secureDataFlow.map { it.textData }.first { it.isNotBlank() } 6. 無限待機の防止 DataStoreはディスクI/Oを伴う非同期処理です。first { it.isNotBlank() } は条件に一致する値が来るまで無限に待機しますが、 もしディスク読み込みが遅延したり、トークンが空のままだと、アプリがフリーズする可能性があり、タイムアウトを追加しました。 7. ProGuard/R8の設定 DataStore 1.1.5以降では、ProGuardルールがライブラリに内包されています。 巷の記事で ProGuardルール の記載が必要なことを目の当たりにしましたが、ルール記載なくリリースビルドしたところクラッシュせず、なぜ? となっていたところ、リリースノート確認し気づきました。 今回、1.1.7 を使用しているため、DataStore専用のProGuardルールを追加する必要はありません。 https://developer.android.com/jetpack/androidx/releases/datastore バージョン1.2.0-beta01で修正された問題として記載: "Fix java.lang.UnsatisfiedLinkError when using DataStore in an app which is optimized with R8" バージョン1.1.5で修正: "missing Proguard rules issue in the Android artifact of datastore-preferences-core" 8. 標準のSharedPreferencesMigrationが使えない EncryptedSharedPreferencesは特殊な暗号化を使用していて、 標準のマイグレーションでは暗号化されたままのデータが転送される また、EncryptedSharedPreferencesは 読み取り時に自動復号化されるのに対し、 今回は CryptographyManagerによる手動暗号化が必要です。 この部分を認知することができず、標準の標準のSharedPreferencesMigrationで実装し、テストしたところ復号できず判明しました。 マイグレーション時に適切な暗号化変換を実装しました。 まとめ 本記事では、Android Keystore、Cipher、DataStoreを組み合わせた秘匿情報の暗号化・永続化の実装について紹介しました。 実装前のディスカッションが重要 : そもそも暗号化が必要なデータかをチームで議論することで、Keystoreへのアクセス頻度を最小限に抑えられた Keystoreの鍵管理 : AES/GCMモードでの鍵生成とIV(初期化ベクトル)の保存を適切に行う必要がある DataStoreとの組み合わせ : Flowベースの非同期読み出しに対応するため、タイムアウトや無限待機の防止策が必要 EncryptedSharedPreferencesからの移行 : 標準のSharedPreferencesMigrationでは暗号化方式の違いにより復号できないため、手動でのマイグレーション実装が必要 Keystoreのクラッシュは完全には避けられませんが、暗号化対象を最適化し、エラーハンドリングを適切に実装することで、安定したセキュアなデータ管理を実現できました。 📣 追記: DataStore 1.3.0-alpha07 で暗号化サポートが追加 本記事の執筆後、 DataStore 1.3.0-alpha07 (2026年3月11日リリース)で、 Tinkライブラリを使用した暗号化サポート が新たに追加されました。 新しい androidx.datastore:datastore-tink アーティファクトにより、 AeadSerializer を使って既存のシリアライザをラップするだけで暗号化が実現できます。 val aeadSerializer = AeadSerializer( aead = keysetHandle.getPrimitive( RegistryConfiguration.get(), Aead::class.java, ), wrappedSerializer = ExistingSerializer, associatedData = "settings.json".encodeToByteArray(), ) 本記事で紹介したKeystore + Cipher による手動実装と比較すると、Tink統合によりボイラープレートが大幅に削減されます。ただし、現時点ではalpha版であるため、プロダクション導入の際はAPIの安定性を考慮する必要があります。今後のstable版リリースに注目です。 参考資料 Android Keystore System Jetpack DataStore 暗号化されたファイルの使用 Android セキュリティのベスト プラクティス DataStore 1.3.0-alpha07 リリースノート















