mirror of
https://github.com/binwiederhier/ntfy-android.git
synced 2024-05-18 19:42:34 +12:00
69 lines
3 KiB
Kotlin
69 lines
3 KiB
Kotlin
package io.heckel.ntfy.util
|
|
|
|
import android.util.Base64
|
|
import io.heckel.ntfy.db.Notification
|
|
import io.heckel.ntfy.db.Subscription
|
|
import io.heckel.ntfy.msg.NotificationParser
|
|
import java.security.MessageDigest
|
|
import javax.crypto.Cipher
|
|
import javax.crypto.SecretKeyFactory
|
|
import javax.crypto.spec.GCMParameterSpec
|
|
import javax.crypto.spec.PBEKeySpec
|
|
import javax.crypto.spec.SecretKeySpec
|
|
|
|
fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
|
|
|
|
object Encryption {
|
|
private const val TAG = "NtfyEncryption"
|
|
private const val keyDerivIter = 50000
|
|
private const val keyLenBits = 256
|
|
private const val gcmTagLenBits = 128
|
|
private const val encodingJwe = "jwe"
|
|
private val parser = NotificationParser()
|
|
|
|
fun maybeDecrypt(subscription: Subscription, notification: Notification): Notification {
|
|
if (notification.encoding != encodingJwe) {
|
|
return notification
|
|
} else if (subscription.encryptionKey == null) {
|
|
Log.w(TAG, "Notification is encrypted, but key is missing: $notification; leaving encrypted message intact")
|
|
return notification
|
|
}
|
|
return try {
|
|
val plaintext = decrypt(notification.message, subscription.encryptionKey)
|
|
val decryptedNotification = parser.parse(plaintext) ?: throw Exception("Cannot parse decrypted message: $plaintext")
|
|
if (decryptedNotification.id != notification.id || decryptedNotification.timestamp != notification.timestamp) {
|
|
throw Exception("Message ID or timestamp mismatch in decrypted message: $plaintext")
|
|
}
|
|
decryptedNotification
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "Unable to decrypt message, falling back to original", e)
|
|
notification
|
|
}
|
|
}
|
|
|
|
fun deriveKey(password: String, topicUrl: String): ByteArray {
|
|
val salt = MessageDigest.getInstance("SHA-256").digest(topicUrl.toByteArray())
|
|
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
|
val spec = PBEKeySpec(password.toCharArray(), salt, keyDerivIter, keyLenBits)
|
|
return factory.generateSecret(spec).encoded
|
|
}
|
|
|
|
fun decrypt(input: String, key: ByteArray): String {
|
|
val parts = input.split(".")
|
|
if (parts.size != 5 || parts[1] != "") {
|
|
throw Exception("Unexpected format")
|
|
}
|
|
val encodedHeader = parts[0]
|
|
val iv = Base64.decode(parts[2], Base64.URL_SAFE)
|
|
val ciphertext = Base64.decode(parts[3], Base64.URL_SAFE)
|
|
val tag = Base64.decode(parts[4], Base64.URL_SAFE)
|
|
val ciphertextWithTag = ciphertext + tag
|
|
val secretKeySpec = SecretKeySpec(key, "AES")
|
|
val gcmParameterSpec = GCMParameterSpec(gcmTagLenBits, iv)
|
|
val cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec)
|
|
cipher.updateAAD(encodedHeader.toByteArray())
|
|
return String(cipher.doFinal(ciphertextWithTag))
|
|
}
|
|
}
|