सामग्री पर जाएं
सभी पोस्ट

2026 में Room डेटाबेस को एन्क्रिप्ट करना: SQLCipher, Keystore, और बिना डेटा खोए माइग्रेशन

Android पर Room/SQLite डेटाबेस को आराम की स्थिति में SQLCipher से एन्क्रिप्ट करने की व्यावहारिक गाइड — Keystore के जरिए की-मैनेजमेंट, एक-बार का माइग्रेशन, और असली परफ़ॉर्मेंस लागत।

MFKAPPS 6 मिनट पढ़ना

एक बायोमेट्रिक प्रॉम्प्ट यह तय करता है कि ऐप कौन खोल सकता है। यह तय नहीं करता कि जब ऐप बंद हो तब डेटाबेस फ़ाइल में क्या पड़ा है। किसी अनलॉक्ड, रूटेड डिवाइस से app.db निकाल लीजिए — या किसी बिना-एन्क्रिप्ट बैकअप से — तो एक सामान्य Room डेटाबेस बिना किसी प्रॉम्प्ट के किसी भी SQLite व्यूअर में खुल जाता है। अगर डेटा फ़िंगरप्रिंट के पीछे लॉक होने लायक है, तो वह आराम की स्थिति में भी एन्क्रिप्ट होने लायक है। ये दो अलग समस्याएँ हैं, और ज़्यादातर गाइड सिर्फ़ पहली को हल करती हैं।

यह वह वर्शन है जो मैंने वाकई शिप किया: Room को रैप करता SQLCipher, हार्डकोडेड स्ट्रिंग की जगह Android Keystore में रखी एक की, और एक माइग्रेशन जो मौजूदा यूज़र्स को बिना एक भी रो खोए प्लेनटेक्स्ट डेटाबेस से एन्क्रिप्टेड डेटाबेस में ले जाता है।

यह बायोमेट्रिक ऑथेंटिकेशन से अलग क्यों है

मैंने पहले एक पोस्ट में Granyn को BiometricPrompt और Keystore-समर्थित CryptoObject से लॉक करने के बारे में लिखा था। वह पैटर्न विशिष्ट फ़ील्ड्स को एन्क्रिप्ट करता है, और सिर्फ़ तब तक जब तक OS यूज़र को उस एक ऑपरेशन के लिए “ऑथेंटिकेटेड” मानता है। स्क्रीन पर दिख रहे बैलेंस को लॉक करने के लिए यह सही टूल है।

यह ख़ुद डेटाबेस फ़ाइल के लिए कुछ नहीं करता। Room का डिफ़ॉल्ट SupportSQLiteOpenHelper डिस्क पर प्लेन SQLite पेज लिखता है — जिसे sqlite3 app.db से पढ़ा जा सकता है, ऑथेंटिकेशन हो या न हो, जिस पल किसी के पास फ़ाइल हो। आराम की स्थिति में पूरा डेटाबेस एन्क्रिप्शन एक अलग लेयर है: यह फ़ाइल की रक्षा करता है, इस बात से बेपरवाह कि कोई ख़ास स्क्रीन लॉक है या नहीं। आमतौर पर आपको दोनों चाहिए होते हैं, पर वे अलग ख़तरों को हल करते हैं, और कोई भी दूसरे की जगह नहीं ले सकता।

SQLCipher को Room से जोड़ना

Android के लिए SQLCipher स्टैंडर्ड SupportSQLiteOpenHelper.Factory के लिए एक ड्रॉप-इन रिप्लेसमेंट देता है, तो Room का सेटअप लगभग नहीं बदलता:

// build.gradle.kts
implementation("net.zetetic:android-database-sqlcipher:4.6.1")
implementation("androidx.sqlite:sqlite-ktx:2.4.0")
fun buildEncryptedDatabase(context: Context, passphrase: ByteArray): AppDatabase {
    val factory = SupportOpenHelperFactory(passphrase)
    return Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
        .openHelperFactory(factory)
        .build()
}

ख़ुद Room के लिए इंटीग्रेशन का पूरा दायरा बस इतना ही है — DAOs, entities, Flow क्वेरीज़, माइग्रेशन सब बिल्कुल पहले जैसे रहते हैं। SQLCipher जो इकलौती नई समस्या लाता है वह वह है जिसे यह आपके लिए हल नहीं करता: passphrase कहाँ से आता है, और इसे इस तरह कैसे स्टोर किया जाए कि यह अब एन्क्रिप्ट हो चुकी फ़ाइल के बगल में रखी एक प्लेनटेक्स्ट की न बन जाए — जो कि सिक्योरिटी थिएटर होगा।

इसे हार्डकोडेड स्ट्रिंग से नहीं, Android Keystore से की देना

पासफ़्रेज़ को कहीं ऐसी जगह रहना चाहिए जिसे ख़ुद OS सुरक्षित रखता हो, किसी रिसोर्स फ़ाइल या BuildConfig स्ट्रिंग में नहीं। Keystore बिल्कुल इसी के लिए बना है: एक AES की जनरेट करें जो हार्डवेयर-समर्थित स्टोरेज से कभी बाहर न निकले, इसे एक रैंडम पासफ़्रेज़ एन्क्रिप्ट करने के लिए इस्तेमाल करें, और सिर्फ़ एन्क्रिप्टेड पासफ़्रेज़ को SharedPreferences या DataStore में स्टोर करें।

private const val KEY_ALIAS = "app_db_passphrase_key"

fun getOrCreateWrappingKey(): SecretKey {
    val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
    keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }

    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
    )
    val spec = KeyGenParameterSpec.Builder(
        KEY_ALIAS,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .build()
    keyGenerator.init(spec)
    return keyGenerator.generateKey()
}

fun getOrCreatePassphrase(prefs: SharedPreferences): ByteArray {
    prefs.getString("wrapped_db_key", null)?.let { stored ->
        return decryptStoredPassphrase(stored, prefs)
    }
    val passphrase = ByteArray(32).also { SecureRandom().nextBytes(it) }
    storeWrappedPassphrase(passphrase, prefs)
    return passphrase
}

ग़ौर कीजिए कि बायोमेट्रिक-सुरक्षित की के उलट, इस की पर setUserAuthenticationRequired(true) नहीं है। यह जानबूझकर है: डेटाबेस को ऐप लॉन्च पर खुलना ज़रूरी है, बैकग्राउंड जॉब से भी, बिना हर बार फ़िंगरप्रिंट प्रॉम्प्ट पर अटके। इस की का काम ज़्यादा सीमित है — पासफ़्रेज़ को डिस्क पर प्लेनटेक्स्ट में न रखना — हर रीड को बायोमेट्रिक्स के पीछे लॉक करना नहीं। अगर किसी स्क्रीन को वह ज़्यादा मज़बूत गारंटी चाहिए, तो पहले वाले CryptoObject पैटर्न को विशिष्ट फ़ील्ड्स पर लेयर करें; एक ही की से दोनों काम करवाने की कोशिश मत कीजिए।

एक-बार का माइग्रेशन: प्लेनटेक्स्ट से एन्क्रिप्टेड तक

मौजूदा यूज़र्स के पास डिस्क पर पहले से ही प्लेनटेक्स्ट app.db है। SQLCipher बस “एन्क्रिप्ट करना शुरू” नहीं कर सकता — आप फ़ाइल को एक बार फिर से लिखते हैं, इसके अपने sqlcipher_export() मेकैनिज़्म का इस्तेमाल करके, जो प्लेनटेक्स्ट सोर्स डेटाबेस के हर टेबल को नए बनाए गए एन्क्रिप्टेड डेटाबेस में कॉपी करता है:

fun migrateToEncrypted(context: Context, passphrase: ByteArray) {
    val plainDbFile = context.getDatabasePath("app.db")
    if (!plainDbFile.exists()) return // नई इंस्टॉल, माइग्रेट करने को कुछ नहीं

    val encryptedPath = context.getDatabasePath("app_encrypted.db").absolutePath
    val plainDb = SQLiteDatabase.openDatabase(
        plainDbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE
    )

    plainDb.rawExecSQL("ATTACH DATABASE '$encryptedPath' AS encrypted KEY '${passphrase.toHexKey()}'")
    plainDb.rawExecSQL("SELECT sqlcipher_export('encrypted')")
    plainDb.rawExecSQL("DETACH DATABASE encrypted")
    plainDb.close()

    val originalRowCount = countRows(plainDbFile.absolutePath)
    val migratedRowCount = countRows(encryptedPath, passphrase)
    check(originalRowCount == migratedRowCount) { "माइग्रेशन के बाद रो काउंट मेल नहीं खाता" }

    plainDbFile.delete()
    File(encryptedPath).renameTo(plainDbFile)
}

रो काउंट पर लगा check() फ़िज़ूल सावधानी नहीं है — यह एक भरोसेमंद माइग्रेशन और एक ऐसे माइग्रेशन के बीच का फ़र्क़ है जिसके फेल होने का पता आपको सपोर्ट ईमेल से चलता है। इसे एक ही बार चलाएँ, DataStore में एक वर्शन फ़्लैग (db_encrypted_v1 = true) के पीछे, Room.databaseBuilder() के डेटाबेस खोलने से पहले। अगर यह एरर दे, तो ओरिजिनल प्लेनटेक्स्ट फ़ाइल मत डिलीट कीजिए — बिना किसी फ़ॉलबैक के आधे-माइग्रेटेड स्टेट का जोखिम लेने के बजाय ऐप को पुराने रास्ते पर रहने दें और इसे लॉग करें।

इसकी क़ीमत क्या है

SQLCipher मुफ़्त नहीं है। मिड-रेंज डिवाइस पर, प्लेन SQLite के मुक़ाबले रीड/राइट थ्रूपुट में लगभग 5-15% ओवरहेड की उम्मीद रखें, मुख्यतः प्रति-पेज AES-256 एन्क्रिप्शन और रीड पर अतिरिक्त HMAC वेरिफ़िकेशन से। कुछ हज़ार रो वाले बजटिंग या सब्सक्रिप्शन-ट्रैकिंग ऐप के लिए, यह महसूस नहीं होता — जो क्वेरी 8ms लेती थीं वे 9ms लेती हैं। किसी एक ट्रांज़ैक्शन में दसियों हज़ार रो के बल्क इम्पोर्ट करने वाली किसी भी चीज़ के लिए, शिप करने से पहले बेंचमार्क कीजिए; वहीं ओवरहेड इतना जमा होता है कि नज़र आने लगे।

दूसरी असली लागत रिकवरी है। रैप्ड पासफ़्रेज़ खो दीजिए — फ़ैक्ट्री रीस्टोर से Keystore रीसेट, कोई OS बग जो ऐप-स्पेसिफ़िक कीज़ मिटा दे — और एन्क्रिप्टेड डेटाबेस डिज़ाइन के हिसाब से अनरिकवरेबल है। कोई छुपा हुआ बैकडोर जोड़ने को नहीं है; “एन्क्रिप्टेड” होने का यही मतलब है। इसे यूज़र-द्वारा-शुरू किए गए बिना-एन्क्रिप्ट एक्सपोर्ट (CSV, प्लेन JSON) के साथ असली बैकअप रणनीति के तौर पर जोड़िए, ताकि “Keystore की चली गई” एक स्थायी डेटा-लॉस न होकर बस एक असुविधा भर रहे।

असल में क्या एन्क्रिप्ट करना चाहिए

हर लोकल-फ़र्स्ट ऐप को इसकी ज़रूरत नहीं होती। SQLCipher की तरफ़ जाने से पहले पूछने लायक सवाल: अगर रॉ फ़ाइल लीक हो जाए तो डेटा क्या उजागर करता है? किसी हाइड्रेशन ऐप का टाइमस्टैम्प लॉग कम-जोखिम वाला है। किसी बजटिंग ऐप के ट्रांज़ैक्शन नोट्स, या किसी मेडिकेशन लॉग की डोज़ हिस्ट्री, वैसी नहीं है — ये वही चीज़ें हैं जिनके बारे में प्राइवेसी पॉलिसी वादे करती है, और “फ़ाइल आराम की स्थिति में AES-256 से एन्क्रिप्टेड थी” एक ऐसा वादा है जिसे SQLCipher सिर्फ़ लिखने ही नहीं, वाक़ई निभाने भी देता है।

// संबंधित पठन

जर्नल से और भी

MFKAPPS 5 मिनट पढ़ना

Granyn बनाना: बिना बैंक लॉगिन वाला बजट ट्रैकर, बस तीन टेबल में

Granyn एक भी बैंक अकाउंट जोड़े बिना कई मुद्राओं में खर्च कैसे ट्रैक करता है और आवर्ती बिलों को कैसे पकड़ता है — इसके पीछे का Room स्कीमा, और इससे जुड़े समझौते।

#android #engineering #privacy
MFKAPPS 8 मिनट पढ़ना

2026 में लोकल-फर्स्ट Android: SQLite, Room, और यूज़र डेटा को डिवाइस पर ही रखना

Room और SQLite के साथ लोकल-फर्स्ट Android ऐप बनाने की 2026 गाइड — स्कीमा डिज़ाइन, माइग्रेशन, WAL, एक्सपोर्ट, और सिंक कब जोड़ें (और कब नहीं)।

#android #engineering #privacy
MFKAPPS 6 मिनट पढ़ना

2026 में Room TypeConverters: अपने स्कीमा को खराब किए बिना एनम, डेट, और लिस्ट स्टोर करना

Android पर Room TypeConverters के लिए एक व्यावहारिक गाइड — एनम, Instant/LocalDate, और लिस्ट — साथ ही वे ग़लतियाँ जो एक कनवर्टर को एक चुपचाप डेटा-करप्शन बग में बदल देती हैं।

#android #engineering #room