PushUtils.kt 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Andy Scherzinger
  5. * @author Marcel Hibbe
  6. * @author Mario Danic
  7. * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  8. * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
  9. * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU General Public License as published by
  13. * the Free Software Foundation, either version 3 of the License, or
  14. * at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. */
  24. package com.nextcloud.talk.utils
  25. import android.content.Context
  26. import android.util.Base64
  27. import android.util.Log
  28. import autodagger.AutoInjector
  29. import com.nextcloud.talk.R
  30. import com.nextcloud.talk.api.NcApi
  31. import com.nextcloud.talk.application.NextcloudTalkApplication
  32. import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
  33. import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
  34. import com.nextcloud.talk.data.user.model.User
  35. import com.nextcloud.talk.events.EventStatus
  36. import com.nextcloud.talk.models.SignatureVerification
  37. import com.nextcloud.talk.models.json.push.PushConfigurationState
  38. import com.nextcloud.talk.models.json.push.PushRegistrationOverall
  39. import com.nextcloud.talk.users.UserManager
  40. import com.nextcloud.talk.utils.UserIdUtils.getIdForUser
  41. import com.nextcloud.talk.utils.preferences.AppPreferences
  42. import io.reactivex.Observer
  43. import io.reactivex.SingleObserver
  44. import io.reactivex.disposables.Disposable
  45. import io.reactivex.schedulers.Schedulers
  46. import org.greenrobot.eventbus.EventBus
  47. import java.io.File
  48. import java.io.FileInputStream
  49. import java.io.FileNotFoundException
  50. import java.io.FileOutputStream
  51. import java.io.IOException
  52. import java.security.InvalidKeyException
  53. import java.security.Key
  54. import java.security.KeyFactory
  55. import java.security.KeyPairGenerator
  56. import java.security.MessageDigest
  57. import java.security.NoSuchAlgorithmException
  58. import java.security.PublicKey
  59. import java.security.Signature
  60. import java.security.SignatureException
  61. import java.security.spec.InvalidKeySpecException
  62. import java.security.spec.PKCS8EncodedKeySpec
  63. import java.security.spec.X509EncodedKeySpec
  64. import java.util.Locale
  65. import javax.inject.Inject
  66. @AutoInjector(NextcloudTalkApplication::class)
  67. class PushUtils {
  68. @JvmField
  69. @Inject
  70. var userManager: UserManager? = null
  71. @Inject
  72. lateinit var appPreferences: AppPreferences
  73. @Inject
  74. lateinit var arbitraryStorageManager: ArbitraryStorageManager
  75. @JvmField
  76. @Inject
  77. var eventBus: EventBus? = null
  78. private val publicKeyFile: File
  79. private val privateKeyFile: File
  80. private val proxyServer: String
  81. init {
  82. sharedApplication!!.componentApplication.inject(this)
  83. val keyPath = sharedApplication!!
  84. .getDir("PushKeystore", Context.MODE_PRIVATE)
  85. .absolutePath
  86. publicKeyFile = File(keyPath, "push_key.pub")
  87. privateKeyFile = File(keyPath, "push_key.priv")
  88. proxyServer = sharedApplication!!
  89. .resources.getString(R.string.nc_push_server_url)
  90. }
  91. fun verifySignature(signatureBytes: ByteArray?, subjectBytes: ByteArray?): SignatureVerification {
  92. val signatureVerification = SignatureVerification()
  93. signatureVerification.signatureValid = false
  94. val users = userManager!!.users.blockingGet()
  95. try {
  96. val signature = Signature.getInstance("SHA512withRSA")
  97. if (users != null && users.size > 0) {
  98. var publicKey: PublicKey?
  99. for (user in users) {
  100. if (user.pushConfigurationState != null) {
  101. publicKey = readKeyFromString(
  102. true,
  103. user.pushConfigurationState!!.userPublicKey
  104. ) as PublicKey?
  105. signature.initVerify(publicKey)
  106. signature.update(subjectBytes)
  107. if (signature.verify(signatureBytes)) {
  108. signatureVerification.signatureValid = true
  109. signatureVerification.user = user
  110. return signatureVerification
  111. }
  112. }
  113. }
  114. }
  115. } catch (e: NoSuchAlgorithmException) {
  116. Log.d(TAG, "No such algorithm")
  117. } catch (e: InvalidKeyException) {
  118. Log.d(TAG, "Invalid key while trying to verify")
  119. } catch (e: SignatureException) {
  120. Log.d(TAG, "Signature exception while trying to verify")
  121. }
  122. return signatureVerification
  123. }
  124. private fun saveKeyToFile(key: Key, path: String): Int {
  125. val encoded = key.encoded
  126. try {
  127. if (!File(path).exists()) {
  128. if (!File(path).createNewFile()) {
  129. return -1
  130. }
  131. }
  132. FileOutputStream(path).use { keyFileOutputStream ->
  133. keyFileOutputStream.write(encoded)
  134. return 0
  135. }
  136. } catch (e: FileNotFoundException) {
  137. Log.d(TAG, "Failed to save key to file")
  138. } catch (e: IOException) {
  139. Log.d(TAG, "Failed to save key to file via IOException")
  140. }
  141. return -1
  142. }
  143. private fun generateSHA512Hash(pushToken: String): String {
  144. var messageDigest: MessageDigest? = null
  145. try {
  146. messageDigest = MessageDigest.getInstance("SHA-512")
  147. messageDigest.update(pushToken.toByteArray())
  148. return bytesToHex(messageDigest.digest())
  149. } catch (e: NoSuchAlgorithmException) {
  150. Log.d(TAG, "SHA-512 algorithm not supported")
  151. }
  152. return ""
  153. }
  154. private fun bytesToHex(bytes: ByteArray): String {
  155. val result = StringBuilder()
  156. for (individualByte in bytes) {
  157. result.append(
  158. Integer.toString((individualByte.toInt() and 0xff) + 0x100, 16)
  159. .substring(1)
  160. )
  161. }
  162. return result.toString()
  163. }
  164. fun generateRsa2048KeyPair(): Int {
  165. if (!publicKeyFile.exists() && !privateKeyFile.exists()) {
  166. var keyGen: KeyPairGenerator? = null
  167. try {
  168. keyGen = KeyPairGenerator.getInstance("RSA")
  169. keyGen.initialize(2048)
  170. val pair = keyGen.generateKeyPair()
  171. val statusPrivate = saveKeyToFile(pair.private, privateKeyFile.absolutePath)
  172. val statusPublic = saveKeyToFile(pair.public, publicKeyFile.absolutePath)
  173. return if (statusPrivate == 0 && statusPublic == 0) {
  174. // all went well
  175. 0
  176. } else {
  177. -2
  178. }
  179. } catch (e: NoSuchAlgorithmException) {
  180. Log.d(TAG, "RSA algorithm not supported")
  181. }
  182. } else {
  183. // We already have the key
  184. return -1
  185. }
  186. // we failed to generate the key
  187. return -2
  188. }
  189. fun pushRegistrationToServer(ncApi: NcApi) {
  190. val pushToken = appPreferences.pushToken
  191. if (pushToken.isNotEmpty()) {
  192. Log.d(TAG, "pushRegistrationToServer will be done with pushToken: $pushToken")
  193. val pushTokenHash = generateSHA512Hash(pushToken).lowercase(Locale.getDefault())
  194. val devicePublicKey = readKeyFromFile(true) as PublicKey?
  195. if (devicePublicKey != null) {
  196. val devicePublicKeyBytes = Base64.encode(devicePublicKey.encoded, Base64.NO_WRAP)
  197. var devicePublicKeyBase64 = String(devicePublicKeyBytes)
  198. devicePublicKeyBase64 = devicePublicKeyBase64.replace("(.{64})".toRegex(), "$1\n")
  199. devicePublicKeyBase64 = "-----BEGIN PUBLIC KEY-----\n$devicePublicKeyBase64\n-----END PUBLIC KEY-----"
  200. val users = userManager!!.users.blockingGet()
  201. for (user in users) {
  202. if (!user.scheduledForDeletion) {
  203. val nextcloudRegisterPushMap: MutableMap<String, String> = HashMap()
  204. nextcloudRegisterPushMap["format"] = "json"
  205. nextcloudRegisterPushMap["pushTokenHash"] = pushTokenHash
  206. nextcloudRegisterPushMap["devicePublicKey"] = devicePublicKeyBase64
  207. nextcloudRegisterPushMap["proxyServer"] = proxyServer
  208. registerDeviceWithNextcloud(ncApi, nextcloudRegisterPushMap, pushToken, user)
  209. }
  210. }
  211. }
  212. } else {
  213. Log.e(TAG, "push token was empty when trying to register at server")
  214. }
  215. }
  216. private fun registerDeviceWithNextcloud(
  217. ncApi: NcApi,
  218. nextcloudRegisterPushMap: Map<String, String>,
  219. token: String,
  220. user: User
  221. ) {
  222. val credentials = ApiUtils.getCredentials(user.username, user.token)
  223. ncApi.registerDeviceForNotificationsWithNextcloud(
  224. credentials,
  225. ApiUtils.getUrlNextcloudPush(user.baseUrl!!),
  226. nextcloudRegisterPushMap
  227. )
  228. .subscribe(object : Observer<PushRegistrationOverall> {
  229. override fun onSubscribe(d: Disposable) {
  230. // unused atm
  231. }
  232. override fun onNext(pushRegistrationOverall: PushRegistrationOverall) {
  233. arbitraryStorageManager.storeStorageSetting(
  234. getIdForUser(user),
  235. LATEST_PUSH_REGISTRATION_AT_SERVER,
  236. System.currentTimeMillis().toString(),
  237. ""
  238. )
  239. Log.d(TAG, "pushTokenHash successfully registered at nextcloud server.")
  240. val proxyMap: MutableMap<String, String?> = HashMap()
  241. proxyMap["pushToken"] = token
  242. proxyMap["deviceIdentifier"] = pushRegistrationOverall.ocs!!.data!!.deviceIdentifier
  243. proxyMap["deviceIdentifierSignature"] = pushRegistrationOverall.ocs!!.data!!.signature
  244. proxyMap["userPublicKey"] = pushRegistrationOverall.ocs!!.data!!.publicKey
  245. registerDeviceWithPushProxy(ncApi, proxyMap, user)
  246. }
  247. override fun onError(e: Throwable) {
  248. Log.e(TAG, "Failed to register device with nextcloud", e)
  249. eventBus!!.post(EventStatus(user.id!!, EventStatus.EventType.PUSH_REGISTRATION, false))
  250. }
  251. override fun onComplete() {
  252. // unused atm
  253. }
  254. })
  255. }
  256. private fun registerDeviceWithPushProxy(ncApi: NcApi, proxyMap: Map<String, String?>, user: User) {
  257. ncApi.registerDeviceForNotificationsWithPushProxy(ApiUtils.getUrlPushProxy(), proxyMap)
  258. .subscribeOn(Schedulers.io())
  259. .subscribe(object : Observer<Unit> {
  260. override fun onSubscribe(d: Disposable) {
  261. // unused atm
  262. }
  263. override fun onNext(t: Unit) {
  264. try {
  265. arbitraryStorageManager.storeStorageSetting(
  266. getIdForUser(user),
  267. LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY,
  268. System.currentTimeMillis().toString(),
  269. ""
  270. )
  271. Log.d(TAG, "pushToken successfully registered at pushproxy.")
  272. updatePushStateForUser(proxyMap, user)
  273. } catch (e: IOException) {
  274. Log.e(TAG, "IOException while updating user", e)
  275. }
  276. }
  277. override fun onError(e: Throwable) {
  278. Log.e(TAG, "Failed to register device with pushproxy", e)
  279. eventBus!!.post(EventStatus(user.id!!, EventStatus.EventType.PUSH_REGISTRATION, false))
  280. }
  281. override fun onComplete() {
  282. // unused atm
  283. }
  284. })
  285. }
  286. @Throws(IOException::class)
  287. private fun updatePushStateForUser(proxyMap: Map<String, String?>, user: User) {
  288. val pushConfigurationState = PushConfigurationState()
  289. pushConfigurationState.pushToken = proxyMap["pushToken"]
  290. pushConfigurationState.deviceIdentifier = proxyMap["deviceIdentifier"]
  291. pushConfigurationState.deviceIdentifierSignature = proxyMap["deviceIdentifierSignature"]
  292. pushConfigurationState.userPublicKey = proxyMap["userPublicKey"]
  293. pushConfigurationState.usesRegularPass = java.lang.Boolean.FALSE
  294. if (user.id != null) {
  295. userManager!!.updatePushState(user.id!!, pushConfigurationState).subscribe(object : SingleObserver<Int?> {
  296. override fun onSubscribe(d: Disposable) {
  297. // unused atm
  298. }
  299. override fun onSuccess(integer: Int) {
  300. eventBus!!.post(
  301. EventStatus(
  302. getIdForUser(user),
  303. EventStatus.EventType.PUSH_REGISTRATION,
  304. true
  305. )
  306. )
  307. }
  308. override fun onError(e: Throwable) {
  309. Log.e(TAG, "update push state for user failed", e)
  310. eventBus!!.post(
  311. EventStatus(
  312. getIdForUser(user),
  313. EventStatus.EventType.PUSH_REGISTRATION,
  314. false
  315. )
  316. )
  317. }
  318. })
  319. } else {
  320. Log.e(TAG, "failed to update updatePushStateForUser. user.getId() was null")
  321. }
  322. }
  323. private fun readKeyFromString(readPublicKey: Boolean, keyString: String?): Key? {
  324. var keyString = keyString
  325. keyString = if (readPublicKey) {
  326. keyString!!.replace("\\n".toRegex(), "").replace(
  327. "-----BEGIN PUBLIC KEY-----",
  328. ""
  329. ).replace("-----END PUBLIC KEY-----", "")
  330. } else {
  331. keyString!!.replace("\\n".toRegex(), "").replace(
  332. "-----BEGIN PRIVATE KEY-----",
  333. ""
  334. ).replace("-----END PRIVATE KEY-----", "")
  335. }
  336. var keyFactory: KeyFactory? = null
  337. try {
  338. keyFactory = KeyFactory.getInstance("RSA")
  339. return if (readPublicKey) {
  340. val keySpec = X509EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT))
  341. keyFactory.generatePublic(keySpec)
  342. } else {
  343. val keySpec = PKCS8EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT))
  344. keyFactory.generatePrivate(keySpec)
  345. }
  346. } catch (e: NoSuchAlgorithmException) {
  347. Log.d(TAG, "No such algorithm while reading key from string")
  348. } catch (e: InvalidKeySpecException) {
  349. Log.d(TAG, "Invalid key spec while reading key from string")
  350. }
  351. return null
  352. }
  353. fun readKeyFromFile(readPublicKey: Boolean): Key? {
  354. val path: String
  355. path = if (readPublicKey) {
  356. publicKeyFile.absolutePath
  357. } else {
  358. privateKeyFile.absolutePath
  359. }
  360. try {
  361. FileInputStream(path).use { fileInputStream ->
  362. val bytes = ByteArray(fileInputStream.available())
  363. fileInputStream.read(bytes)
  364. val keyFactory = KeyFactory.getInstance("RSA")
  365. return if (readPublicKey) {
  366. val keySpec = X509EncodedKeySpec(bytes)
  367. keyFactory.generatePublic(keySpec)
  368. } else {
  369. val keySpec = PKCS8EncodedKeySpec(bytes)
  370. keyFactory.generatePrivate(keySpec)
  371. }
  372. }
  373. } catch (e: FileNotFoundException) {
  374. Log.d(TAG, "Failed to find path while reading the Key")
  375. } catch (e: IOException) {
  376. Log.d(TAG, "IOException while reading the key")
  377. } catch (e: InvalidKeySpecException) {
  378. Log.d(TAG, "InvalidKeySpecException while reading the key")
  379. } catch (e: NoSuchAlgorithmException) {
  380. Log.d(TAG, "RSA algorithm not supported")
  381. }
  382. return null
  383. }
  384. companion object {
  385. private const val TAG = "PushUtils"
  386. const val LATEST_PUSH_REGISTRATION_AT_SERVER: String = "LATEST_PUSH_REGISTRATION_AT_SERVER"
  387. const val LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY: String = "LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY"
  388. }
  389. }