-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathFcmService.kt
More file actions
184 lines (157 loc) · 5.96 KB
/
FcmService.kt
File metadata and controls
184 lines (157 loc) · 5.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package to.bitkit.fcm
import android.os.Bundle
import androidx.core.os.toPersistableBundle
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonObject
import to.bitkit.data.keychain.Keychain
import to.bitkit.di.json
import to.bitkit.env.Env.derivationName
import to.bitkit.ext.fromBase64
import to.bitkit.ext.fromHex
import to.bitkit.models.BlocktankNotificationType
import to.bitkit.repositories.LightningRepo
import to.bitkit.ui.pushNotification
import to.bitkit.utils.Crypto
import to.bitkit.utils.Logger
import java.util.Date
import javax.inject.Inject
@AndroidEntryPoint
class FcmService : FirebaseMessagingService() {
companion object {
private const val TAG = "FcmService"
}
private var notificationType: BlocktankNotificationType? = null
private var notificationPayload: JsonObject? = null
@Inject
lateinit var crypto: Crypto
@Inject
lateinit var keychain: Keychain
@Inject
lateinit var lightningRepo: LightningRepo
/**
* Act on received messages. [Debug](https://goo.gl/39bRNJ)
*/
override fun onMessageReceived(message: RemoteMessage) {
Logger.debug("New FCM at: ${Date(message.sentTime)}", context = TAG)
message.notification?.run {
Logger.debug("FCM title: $title", context = TAG)
Logger.debug("FCM body: $body", context = TAG)
sendNotification(title, body, Bundle(message.data.toPersistableBundle()))
}
if (message.data.isNotEmpty()) {
Logger.debug("FCM data: ${message.data}", context = TAG)
val shouldSchedule = runCatching {
val isEncryptedNotification = message.data.tryAs<EncryptedNotification> {
decryptPayload(it)
}
isEncryptedNotification
}.getOrElse {
Logger.error("Failed to read encrypted notification payload", it, context = TAG)
// Let the node to spin up and handle incoming events
true
}
when (shouldSchedule) {
true -> handleAsync()
else -> handleNow(message.data)
}
}
}
private fun handleAsync() {
val work = OneTimeWorkRequestBuilder<WakeNodeWorker>()
.setInputData(
workDataOf(
"type" to notificationType?.name,
"payload" to notificationPayload?.toString(),
)
)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(this)
.beginWith(work)
.enqueue()
}
private fun handleNow(data: Map<String, String>) {
Logger.warn("FCM handler not implemented for: $data", context = TAG)
}
@Suppress("ReturnCount")
private fun decryptPayload(response: EncryptedNotification) {
val ciphertext = runCatching { response.cipher.fromBase64() }.getOrElse {
Logger.error("Failed to decode cipher", it, context = TAG)
return
}
val privateKey = runCatching { keychain.load(Keychain.Key.PUSH_NOTIFICATION_PRIVATE_KEY.name)!! }.getOrElse {
Logger.error("Missing PUSH_NOTIFICATION_PRIVATE_KEY", it, context = TAG)
return
}
val password =
runCatching { crypto.generateSharedSecret(privateKey, response.publicKey, derivationName) }.getOrElse {
privateKey.fill(0) // Wipe on error path
Logger.error("Failed to generate shared secret", it, context = TAG)
return
}
// Wipe private key after use
privateKey.fill(0)
val decrypted = crypto.decrypt(
encryptedPayload = Crypto.EncryptedPayload(ciphertext, response.iv.fromHex(), response.tag.fromHex()),
secretKey = password,
)
// Wipe password after use
password.fill(0)
val decoded = decrypted.decodeToString()
Logger.debug("Decrypted payload: $decoded", context = TAG)
val (payload, type) = runCatching { json.decodeFromString<DecryptedNotification>(decoded) }.getOrElse {
Logger.error("Failed to decode decrypted data", it, context = TAG)
return
}
if (payload == null) {
Logger.error("Missing payload", context = TAG)
return
}
if (type == null) {
Logger.error("Missing type", context = TAG)
return
}
notificationType = type
notificationPayload = payload
}
private fun sendNotification(title: String?, body: String?, extras: Bundle? = null) {
applicationContext.pushNotification(title, body, extras)
}
private inline fun <reified T> Map<String, String>.tryAs(block: (T) -> Unit): Boolean {
val encoded = json.encodeToString(this)
return try {
val decoded = json.decodeFromString<T>(encoded)
block(decoded)
true
} catch (_: SerializationException) {
false
}
}
override fun onNewToken(token: String) {
Logger.debug("Got new FCM token via FirebaseMessagingService.onNewToken: '$token'", context = TAG)
lightningRepo.registerForNotificationsAsync(token)
}
}
@Serializable
data class EncryptedNotification(
val cipher: String,
val iv: String,
val tag: String,
val sound: String = "",
val title: String = "",
val message: String = "",
val publicKey: String = "",
)
@Serializable
data class DecryptedNotification(
val payload: JsonObject? = null,
val type: BlocktankNotificationType? = null,
)