Complete secure log viewing and retention
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package se.ajpanton.notificationlog.data
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import java.io.File
|
||||
import java.io.DataInputStream
|
||||
import java.io.FileNotFoundException
|
||||
@@ -12,11 +13,19 @@ class EncryptedImageStore(context: Context) {
|
||||
|
||||
fun save(id: String, bytes: ByteArray) {
|
||||
require(id.matches(IMAGE_ID_PATTERN)) { "Invalid notification image ID." }
|
||||
require(bytes.size <= MAX_IMAGE_BYTES) { "Notification image exceeds the 5 MiB retention limit." }
|
||||
val payload = cipher.encrypt(bytes)
|
||||
File(directory, "$id.bin").outputStream().use { output ->
|
||||
val file = AtomicFile(File(directory, "$id.bin"))
|
||||
val output = file.startWrite()
|
||||
try {
|
||||
output.write(payload.initializationVector.size)
|
||||
output.write(payload.initializationVector)
|
||||
output.write(payload.cipherText)
|
||||
output.flush()
|
||||
file.finishWrite(output)
|
||||
} catch (error: Exception) {
|
||||
file.failWrite(output)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +47,13 @@ class EncryptedImageStore(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(id: String) {
|
||||
require(id.matches(IMAGE_ID_PATTERN)) { "Invalid notification image ID." }
|
||||
File(directory, "$id.bin").delete()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val IMAGE_ID_PATTERN = Regex("[0-9a-f-]{36}")
|
||||
const val MAX_IMAGE_BYTES = 5 * 1024 * 1024
|
||||
}
|
||||
}
|
||||
|
||||
+29
-1
@@ -27,7 +27,9 @@ class EncryptedNotificationLogStore(context: Context) {
|
||||
|
||||
fun append(entry: NotificationLogEntry) = synchronized(lock) {
|
||||
val entries = (readPayloadOrNull()?.let { NotificationLogEntryJson.decode(cipher.decrypt(it)) } ?: emptyList())
|
||||
write(entries + entry)
|
||||
val retained = retain(entries + entry)
|
||||
write(retained)
|
||||
(entries.mapNotNull { it.imageId } - retained.mapNotNull { it.imageId }.toSet()).forEach(::deleteImage)
|
||||
}
|
||||
|
||||
fun replaceAll(entries: List<NotificationLogEntry>) = synchronized(lock) {
|
||||
@@ -39,6 +41,14 @@ class EncryptedNotificationLogStore(context: Context) {
|
||||
imageDirectory.listFiles()?.forEach(File::delete)
|
||||
}
|
||||
|
||||
fun delete(id: String): Boolean = synchronized(lock) {
|
||||
val entries = readPayloadOrNull()?.let { NotificationLogEntryJson.decode(cipher.decrypt(it)) } ?: return false
|
||||
val entry = entries.firstOrNull { it.id == id } ?: return false
|
||||
write(entries.filterNot { it.id == id })
|
||||
entry.imageId?.let(::deleteImage)
|
||||
true
|
||||
}
|
||||
|
||||
private fun write(entries: List<NotificationLogEntry>) {
|
||||
val payload = cipher.encrypt(NotificationLogEntryJson.encode(entries))
|
||||
val output = file.startWrite()
|
||||
@@ -57,6 +67,21 @@ class EncryptedNotificationLogStore(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun retain(entries: List<NotificationLogEntry>): List<NotificationLogEntry> {
|
||||
var retained = entries.takeLast(MAX_ENTRIES)
|
||||
while (retained.size > 1 && NotificationLogEntryJson.encode(retained).size > MAX_PLAINTEXT_BYTES) {
|
||||
retained = retained.drop(1)
|
||||
}
|
||||
require(NotificationLogEntryJson.encode(retained).size <= MAX_PLAINTEXT_BYTES) {
|
||||
"A notification event exceeds the encrypted log retention limit."
|
||||
}
|
||||
return retained
|
||||
}
|
||||
|
||||
private fun deleteImage(id: String) {
|
||||
if (id.matches(IMAGE_ID_PATTERN)) File(imageDirectory, "$id.bin").delete()
|
||||
}
|
||||
|
||||
private fun readPayloadOrNull(): EncryptedPayload? {
|
||||
val input = try {
|
||||
file.openRead()
|
||||
@@ -82,6 +107,9 @@ class EncryptedNotificationLogStore(context: Context) {
|
||||
const val FILE_VERSION = 1
|
||||
const val MAX_IV_BYTES = 32
|
||||
const val MAX_CIPHER_TEXT_BYTES = 50 * 1024 * 1024
|
||||
const val MAX_PLAINTEXT_BYTES = 20 * 1024 * 1024
|
||||
const val MAX_ENTRIES = 10_000
|
||||
const val IMAGE_DIRECTORY_NAME = "notification-images"
|
||||
val IMAGE_ID_PATTERN = Regex("[0-9a-f-]{36}")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user