Ders 08 / 14
Şifreli Depolama ve Anahtar Deposu
Uygulamanın yazdığı sır anahtar deposundaki bir anahtarla şifrelenir; anahtar kullanıcı doğrulamasına bağlıysa kilit ekranı kalkınca geçersizleşir ve kayıt okunamaz. Ders yazılan sırrın sonraki okumaya kaç kopya olarak ulaştığını ölçer.
Yayın Güncelleme
Bu derste
- Anahtar deposunda anahtar takma adının kayıtla ilişkisini açıklar
- Kullanıcı doğrulamasına bağlı anahtarın geçersizleşince kaydı okunamaz bıraktığını gösterir
- Okunamayan kaydın sonraki okumaya ulaşmadığını kayıp olarak ölçer
- Aynı anahtarla yeniden yazılan sırrın eski kaydı ezilen yaptığını, kuralın nedeni görmediğini ayırır
İçindekiler
Önceki derste olaylar aygıtın içinde bir ekrana kadar ilerliyordu. Bu derste olay bir ekrana değil, bir depoya yazılır: uygulama bir sır üretir, saklar ve sonraki oturumda geri okur. Yazan ile okuyan aynı uygulamadır, ama aralarında zaman ve bir anahtar vardır. Kursun açılış dersinde sekiz olayın dağılımı birebir 3, yinelenen 1, ezilen 2, kayıp 2 olarak ölçülmüştü. Bu derste sorulan soru aynıdır: gönderilen bu olay alıcıya kaç kopya olarak ulaştı? Olay yazılan sırdır, alıcı sonraki okumadır.
// Arrival.kt — gonderilen bir olayin aliciya kac kopya olarak ulastigini siniflandirir
class Signal(val key: String, val seq: Int)
class Arrival(val label: String, val sent: Signal, received: List<Signal>) {
val copies: Int = received.count { it.seq == sent.seq }
val verdict: String = when {
copies > 1 -> "yinelenen"
copies == 1 -> "birebir"
received.any { it.key == sent.key && it.seq > sent.seq } -> "ezilen"
else -> "kayip"
}
fun line() {
println("$label: anahtar=${sent.key} | kopya=$copies -> $verdict")
}
}
fun countArrivals(arrivals: List<Arrival>) {
for (name in listOf("birebir", "yinelenen", "ezilen", "kayip")) {
println("$name: ${arrivals.count { it.verdict == name }}")
}
}
Kural kopya sayısına bakar. Aynı seq ile bir kopya ulaştıysa teslim birebirdir, birden çoksa yinelenendir. Kopya yoksa ve aynı anahtarla daha büyük bir seq ulaştıysa olay ezilmiştir. Hiçbiri yoksa kayıptır. Kural içeriği, nedeni ve kaydın şifreli olup olmadığını görmez.
Anahtar Deposu ve Takma Ad
Platformun anahtar deposu (KeyStore), şifreleme anahtarlarını uygulamanın belleğinden ve dosyalarından ayrı tutar. Uygulama anahtarın kendisini görmez; bir anahtar takma adı (alias) ile ona seslenir ve işlemi anahtarı görmeden ister. Takma ad bir kayıt defterinin satır başlığı gibidir: aynı ad altında bir anahtar bulunur ve şifrelenen her kayıt hangi takma adın anahtarıyla şifrelendiğini bilir.
Anahtar üretilirken bir koşul eklenebilir: anahtar yalnız kullanıcı doğrulandıktan sonra kullanılabilsin.
// Platform kodu: KeyGenParameterSpec
val spec = KeyGenParameterSpec.Builder(
"kilit",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.build()
val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
generator.init(spec)
val key = generator.generateKey()
setUserAuthenticationRequired(true) anahtarı kullanıcı doğrulamasına bağlar. Platformun belgesine göre böyle bir anahtar, güvenli kilit ekranı kaldırılınca ya da zorla sıfırlanınca geri dönüşsüz biçimde geçersizleşir. Geçersizleşmiş anahtarla işlem başlatan kod bir istisna alır.
// Platform kodu: KeyPermanentlyInvalidatedException
try {
cipher.init(Cipher.DECRYPT_MODE, key)
} catch (e: KeyPermanentlyInvalidatedException) {
keyStore.deleteEntry("kilit")
}
Şifreli tercih deposu bu anahtarı hazır bir kabukla sarar. Uygulama değeri yazar, kabuk anahtarı kullanarak şifreler.
// Platform kodu: EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val prefs = EncryptedSharedPreferences.create(
context,
"sirlar",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
prefs.edit().putString("sir.kart", "gizli-deger").apply()
Bu şifreli tercih deposu kitaplığı kullanımdan kaldırılmıştır; yeni kodda örnek alınmamalıdır. Güncel yol, anahtarı doğrudan anahtar deposunda üretip veriyi uygulamanın kendisinin şifrelemesi ya da veri deposunu bir şifreleme kitaplığıyla birlikte kullanmaktır.
Bu üç parça platform kodudur ve burada çıktı, süre ya da sayı iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir. Modelde kasa, kayıtları takma ada bağlı tutar ve bir takma adın anahtarı geçersizleşince o adın kayıtları okunamaz.
// Vault.kt — kayitlari anahtar takma adina baglar, gecersizlesen anahtarin kayitlarini okutmaz
class Vault {
private val records = mutableListOf<Pair<String, Signal>>()
private val invalid = mutableSetOf<String>()
fun write(alias: String, secret: Signal) {
records.add(alias to secret)
}
fun invalidateKey(alias: String) {
invalid.add(alias)
}
fun migrate(from: String, to: String) {
records.filter { it.first == from }.forEach { records.add(to to it.second) }
}
fun readAll(): List<Signal> = records.filter { it.first !in invalid }.map { it.second }
}
Alıcı, readAll işlevinin dönüşüdür. Her ölçümde uygulama sırları yazar ve sonra geri okur.
// Probe.kt — gonderilen sirlar icin kasadan okunan listeyi alici sayar
fun probe(vault: Vault, sent: List<Signal>): List<Arrival> {
val received = vault.readAll()
return sent.mapIndexed { i, s -> Arrival("olay-${i + 1}", s, received) }
}
Anahtar Geçerliyken
Üç sır aynı takma adla yazılır ve anahtara dokunulmaz. Anahtar geçerli olduğu sürece okuma her kaydı döndürür.
// Main.kt — anahtar gecerli kaldikca her kayit okunur
fun main() {
val vault = Vault()
val sent = listOf(Signal("sir.oturum", 1), Signal("sir.kart", 2), Signal("sir.not", 3))
sent.forEach { vault.write("kilit", it) }
val arrivals = probe(vault, sent)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=sir.oturum | kopya=1 -> birebir olay-2: anahtar=sir.kart | kopya=1 -> birebir olay-3: anahtar=sir.not | kopya=1 -> birebir birebir: 3 yinelenen: 0 ezilen: 0 kayip: 0
Üç yazının üçü okunur. Şifreli depolamanın sağladığı güvence burada yoktur: kasa, kaydın şifreli olup olmadığını modellemez, yalnız okunup okunamadığını.
Kilit Ekranı Kalkınca
Dört sır iki takma adın altında durur. Bir kısmı kullanıcı doğrulamasına bağlı kilit anahtarıyla, bir kısmı bağlı olmayan genel anahtarıyla şifrelenmiştir. Kullanıcı kilit ekranını kaldırır ve kilit anahtarı geçersizleşir.
// Main.kt — gecersizlesen anahtarin kayitlari okunamaz
fun main() {
val vault = Vault()
val sent = listOf(
Signal("sir.oturum", 1),
Signal("sir.kart", 2),
Signal("sir.not", 3),
Signal("sir.ayar", 4),
)
vault.write("genel", sent[0])
vault.write("kilit", sent[1])
vault.write("kilit", sent[2])
vault.write("genel", sent[3])
vault.invalidateKey("kilit")
val arrivals = probe(vault, sent)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=sir.oturum | kopya=1 -> birebir olay-2: anahtar=sir.kart | kopya=0 -> kayip olay-3: anahtar=sir.not | kopya=0 -> kayip olay-4: anahtar=sir.ayar | kopya=1 -> birebir birebir: 2 yinelenen: 0 ezilen: 0 kayip: 2
Kayıtlar diskte durmaya devam eder, ama okuma onları döndürmez. Kural bunu kayıp sayar: olay yazıldı, alıcıya hiçbir kopya ulaşmadı ve aynı anahtarla daha büyük bir seq da gelmedi. Bu dersin teslimi: kayıp. Sebebi, kaydın var olması ile okunabilir olmasının aynı şey olmamasıdır.
Taşıma Kopya Bırakırsa
Uygulama kullanıcı doğrulamasına bağlı olmayan kayıtları yeni bir takma ada taşımak ister. Taşıma kayıtları yeni ada kopyalar, eskileri silmez. Eski anahtar geçerli kaldığı için iki ad altındaki kayıt da okunur.
// Main.kt — tasima eski kaydi silmezse okuma iki kopya dondurur
fun main() {
val vault = Vault()
val sent = listOf(Signal("sir.oturum", 1), Signal("sir.kart", 2), Signal("sir.not", 3))
vault.write("genel", sent[0])
vault.write("genel", sent[1])
vault.write("kilit", sent[2])
vault.migrate("genel", "genel-2")
val arrivals = probe(vault, sent)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=sir.oturum | kopya=2 -> yinelenen olay-2: anahtar=sir.kart | kopya=2 -> yinelenen olay-3: anahtar=sir.not | kopya=1 -> birebir birebir: 1 yinelenen: 2 ezilen: 0 kayip: 0
Taşınan iki sır iki kopya olarak ulaştı. Okuma kodu bu kopyaları birleştirmiyorsa uygulama aynı sırrı iki kez görür. Yinelenen sonuç, taşımanın yarım kaldığını gösteren bir işarettir; kural ise yalnız sayıyı verir.
GD1. Model, her kaydı tek bir takma adın anahtarıyla ilişkilendirir. Platformda bir kayıt, anahtarın sürümüne, şifreleme kipine ve başlatma vektörüne de bağlıdır; bunlar modelde yoktur.
GD2. Model, anahtarın geçersizleşmesini tek bir çağrı olarak kurar. Platformda geçersizleşme, kilit ekranının kaldırılması ya da zorla sıfırlanması gibi bir olaydan sonra anahtar kullanılmak istendiğinde fark edilir.
Geçersizleşmenin Sınırları
Geçersizleşmenin neden bir kayıp ürettiğini anlamak için üç ayrımı birbirinden ayırmak gerekir. Birincisi, anahtar ile kayıt ayrı şeylerdir: kayıt uygulamanın dosyasında durur, anahtar ise anahtar deposunda. Anahtar gidince kayıt yerinde kalır, ama onu açacak bir şey kalmaz. İkincisi, anahtarı uygulama yaratmış olsa da uygulama onu geri getiremez; yalnız aynı takma adla yeni bir anahtar üretebilir ve yeni anahtar eski kaydı açmaz. Üçüncüsü, geçersizleşme uygulamanın kontrolünde bir olay değildir. Tetikleyen, kullanıcının kilit ekranıyla yaptığı bir işlemdir ve uygulama bunu ancak sonraki okumada, anahtarı kullanmak istediğinde fark eder.
Bu üç ayrımın modeldeki karşılığı basittir. Kasa her kaydı bir takma adla işaretler. invalidateKey bir takma adı geçersiz sayar. readAll geçersiz takma adların kayıtlarını döndürmez. Modelde kaydın içeriği hiçbir zaman görünmez; kural yalnız dönen listeye bakar.
Burada platformun söylediği ile modelin söylediği arasındaki fark açık kalmalıdır. Platform, kullanıcı doğrulamasına bağlı bir anahtarın kilit ekranı kaldırılınca geçersizleştiğini belgeler. Anahtarın başka hangi koşullarda geçersizleşebileceği anahtarın üretim parametrelerine bağlıdır ve bu derste ölçülmez. Model yalnız invalidateKey çağrısını bilir.
Sınırlayıcı Ölçüm
Kasa, kaybın nedenini taşımaz. Kilit ekranı kalkınca kilit anahtarı geçersizleşir ve iki sır okunamaz olur. Uygulama bunu fark edip yeni bir anahtar üretir ve yalnız kartı yeniden yazar: aynı anahtar adı sir.kart, bu kez yeni bir seq ve yeni bir takma adla.
// Main.kt — ayni anahtara yeniden yazilan sir eski kaydi ezer
fun main() {
val vault = Vault()
val sent = listOf(
Signal("sir.oturum", 1),
Signal("sir.kart", 2),
Signal("sir.not", 3),
Signal("sir.kart", 4),
)
vault.write("genel", sent[0])
vault.write("kilit", sent[1])
vault.write("kilit", sent[2])
vault.invalidateKey("kilit")
vault.write("kilit-2", sent[3])
val arrivals = probe(vault, sent)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=sir.oturum | kopya=1 -> birebir olay-2: anahtar=sir.kart | kopya=0 -> ezilen olay-3: anahtar=sir.not | kopya=0 -> kayip olay-4: anahtar=sir.kart | kopya=1 -> birebir birebir: 2 yinelenen: 0 ezilen: 1 kayip: 1
Okunamayan iki sırrın ikisi de aynı nedenle, geçersizleşen aynı anahtar yüzünden okunamaz. Buna karşın kurala göre biri ezilen, öteki kayıptır. sir.kart için aynı anahtarla daha büyük bir seq ulaştığı için ezilendir; sir.not için böyle bir kopya olmadığı için kayıptır. Kural kaybın nedenini görmez, yalnız yerini alan bir kopya olup olmadığını görür.
Bu ayrım uygulama açısından önemlidir. Ezilen bir sır, kullanıcı için kaybolmuş sayılmaz; yeni sürümü okunur. Kayıp bir sır ise yeniden girilmesi gereken bir veridir. Kural ikisini ayırır ama hangisinin kullanıcıya daha pahalıya geldiğini söylemez.
Kayıp, kullanıcının eylemiyle ezilene dönüşebilir. Kullanıcı geçersizleşmeden sonra kartı ve notu yeniden girer; uygulama ikisini yeni takma adla, yeni seq değerleriyle yazar.
// Main.kt — kullanici sirlari yeniden girerse kayiplar ezilene doner
fun main() {
val vault = Vault()
val sent = listOf(
Signal("sir.oturum", 1),
Signal("sir.kart", 2),
Signal("sir.not", 3),
Signal("sir.kart", 4),
Signal("sir.not", 5),
)
vault.write("genel", sent[0])
vault.write("kilit", sent[1])
vault.write("kilit", sent[2])
vault.invalidateKey("kilit")
vault.write("kilit-2", sent[3])
vault.write("kilit-2", sent[4])
val arrivals = probe(vault, sent)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=sir.oturum | kopya=1 -> birebir olay-2: anahtar=sir.kart | kopya=0 -> ezilen olay-3: anahtar=sir.not | kopya=0 -> ezilen olay-4: anahtar=sir.kart | kopya=1 -> birebir olay-5: anahtar=sir.not | kopya=1 -> birebir birebir: 3 yinelenen: 0 ezilen: 2 kayip: 0
Önceki ölçümde kayıp olan sir.not artık ezilendir, çünkü yerini alan daha büyük bir seq ulaştı. Okunamayan eski kayıtlar yerinde durur; sayım değişti, çünkü kural artık yerini alan bir kopya görüyor. Kullanıcıya yeniden girdirmek yeniden üretilebilen sırlar için bir toparlanma yoludur. Yeniden girilemeyen bir sır için ise aynı ölçüm kayıp olarak kalır.
GD3. Model, yeniden yazmayı yeni bir takma adla yapar ve yeni sırrı eskisinin üzerine koymaz. Platformda aynı takma adla yeni bir anahtar üretmek de mümkündür; model bu ayrımı kurmaz.
Okuyan Kodun Yapacakları
Alıcı tarafında yapılacaklar ölçümden çıkar. Okuma sonucu beklenenden kısaysa bunun bir hata değil bir teslim sonucu olduğu kabul edilir ve üç şey yapılır.
- Eksiği tanımak. Okuyan kod, hangi sırları beklediğini bilmelidir. Beklenen listeyi okunan listeyle karşılaştırmadan kayıp fark edilmez. Modelde bu karşılaştırmayı
probeyapar. - Kopyaları birleştirmek. Aynı anahtar için birden çok kayıt dönüyorsa, en büyük
seqdeğerli olan alınır. Taşıma kopya bırakabildiği için okuma kodu kopyayı hata saymaz, ama iki kez işlemez de. - Yeniden üretilebilirliği ayırmak. Sunucudan yeniden alınabilen bir oturum anahtarı ile yalnız kullanıcının bildiği bir parola aynı kayıp sınıfında değildir. İlki sessizce yenilenir, ikincisi kullanıcıdan istenir. Bu ayrım yazma anında, sır depoya girmeden önce yapılmalıdır.
Okuma sonucunun boş dönmesi ile hata vermesi de ayrı ele alınır. Boş sonuç hiç kayıt yazılmadığını, hata ise kaydın var olduğunu ama açılamadığını söyleyebilir; ikisini aynı dala koyan kod, geçersizleşmeyi ilk açılış sanır.
Okuma kodunun yapmaması gereken bir şey de vardır: okunamayan kaydı sessizce silmek. Kayıt silinirse sonraki ölçümde ne ezilen ne kayıp ayırt edilir; hiçbir iz kalmaz.
Yaygın Yanılgılar
Şifreli kayıt her zaman okunur. Şifreleme kaydı korur, ama okunabilirliğini garanti etmez. Anahtar kaybolursa kayıt korunmuş ve ulaşılamaz olur. İki ölçüm bunu gösterdi: anahtar geçerliyken üç kayıt birebir, geçersizleşince iki kayıt kayıp.
Diskte duran kayıt teslim edilmiştir. Teslim, alıcıya ulaşan kopyadır. Diskteki okunamayan kayıt alıcıya ulaşmaz. Kural bu yüzden diskteki baytlara değil, okumanın dönüşüne bakar.
Ezilen kötü haberdir. Ezilen, yerine yenisi ulaştığı için çoğu zaman iyi haberdir. Kötü olan, ezen kopyanın hiç gelmemesidir; o durumda aynı olay kayıp olarak sayılır. Son ölçüm ikisini yan yana gösterir.
Yinelenen veri bozulmasıdır. Yinelenen yalnız aynı kopyanın birden çok kez ulaşmasıdır. Taşıma sırasında bırakılan kopya bir bozulma değildir, tamamlanmamış bir temizliktir.
Kayıt yedeklenirse anahtar da yedeklenmiş olur. Olmaz. Kayıt yalnız şifreli baytlardır; onu açan anahtar kayıttan ayrı durur. Kaydı taşıyıp anahtarı taşımayan bir düzende okuma, ölçümdeki geçersizleşmeyle aynı sonucu verir: kayıtlar vardır, döndürülmez, kural kayıp sayar. Platformun yedekleme davranışı bu derste ölçülmez.
Anahtarı yeniden üretmek kaydı geri getirir. Getirmez. Aynı takma adla yeni bir anahtar üretmek, eski anahtarla şifrelenmiş kaydı açmaz; yalnız bundan sonra yazılacak kayıtlar için çalışan bir anahtar bırakır.
Özet
- Bu dersin teslimi: kayıp. Kilit ekranı kalkınca kullanıcı doğrulamasına bağlı anahtar geçersizleşir ve o anahtarla şifrelenmiş kayıt okunamaz.
- Anahtar takma adı kaydı anahtara bağlar; aynı adın altındaki kayıtlar birlikte okunamaz hâle gelir.
- Geçerli anahtarda üç yazının üçü birebir ulaşır. Kullanıcı doğrulamasına bağlı iki sır geçersizleşince kayıp 2, birebir 2 olur.
- Taşıma eski kaydı silmezse iki sır iki kopya olarak okunur: yinelenen 2, birebir 1.
- Aynı anahtarla yeniden yazılan sır eski kaydı ezilen yapar; yeniden yazılmayan sır kayıptır. Kullanıcı iki sırrı da yeniden girerse ezilen 2, birebir 3 olur. Kural nedeni değil kopyaları görür.
Sonraki Adım
Bu derste kayıt diske yazıldı ve anahtar geçersizleşince okunamadı. Sonraki derste olay ağa çıkar: uygulama bir isteği gönderir, ama istek düz metin diye engellenirse hiçbir kopya sunucuya ulaşmaz. Ağ güvenliği yapılandırması bu engelin kuralını yazar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.