// Copyright 2019-2021 The Hush developers package org.myhush.silentdragon import android.content.Context import android.util.Log import com.beust.klaxon.JsonObject import com.beust.klaxon.Klaxon import com.beust.klaxon.Parser import com.beust.klaxon.json import okhttp3.WebSocket import org.libsodium.jni.NaCl import org.libsodium.jni.Sodium import java.math.BigInteger object DataModel { class MainResponse(val balance: Double, val maxspendable: Double, val maxzspendable: Double? = null, val saplingAddress: String, val tAddress: String, val tokenName: String, val serverversion: String) class TransactionItem(val type: String, val datetime: Long, val amount: String, val memo: String?, val addr: String, val txid: String?, val confirmations: Long) var mainResponseData : MainResponse? = null var transactions : List? = null val fee: Double = 0.0001 val currencyValues: HashMap = HashMap() val currencySymbols: HashMap = HashMap() var selectedCurrency = "" fun isTestnet(): Boolean { return mainResponseData?.tokenName != "HUSH" } var ws : WebSocket? = null enum class ConnectionStatus(val status: Int) { DISCONNECTED(1), CONNECTING(2), CONNECTED(3) } var connStatus: ConnectionStatus = ConnectionStatus.DISCONNECTED fun clear() { mainResponseData = null transactions = null } fun ByteArray.toHexString() : String { return (joinToString("") { String.format("%02x", it) }) } fun String.hexStringToByteArray(byteSize: Int) : ByteArray { val s = "00".repeat(byteSize - (this.length / 2) ) + this return ByteArray(byteSize) { s.substring(it * 2, it * 2 + 2).toInt(16).toByte() } } fun init() { val sodium = NaCl.sodium() } data class ParseResponse(val updateTxns: Boolean = false, val displayMsg: String? = null, val doDisconnect: Boolean = false) // Parse the encrypted response string. This will decrypt it and pass it to parseDecryptedResponse() fun parseResponse(response: String) : ParseResponse { val json = Parser.default().parse(StringBuilder(response)) as JsonObject // Check if it has errored out if (json.containsKey("error")) { return ParseResponse( false, "Couldn't connect: ${json["error"].toString()}", true ) } if (json.containsKey("ping")) { return ParseResponse(false) } // Check if input string is encrypted if (json.containsKey("nonce")) { val decrypted = decrypt( json["nonce"].toString(), json["payload"].toString() ) if (decrypted.startsWith("error")) { return ParseResponse( false, "Encryption Error: $decrypted", true ) } return parseDecryptedResponse(decrypted) } else { // The input is unrecognized return ParseResponse( false, "Unknown message received, JSON was missing a 'nonce'", true ) } } // Parse a decrypted response, which is expected to have a command's response fun parseDecryptedResponse(response: String): ParseResponse { val json = Parser.default().parse(StringBuilder(response)) as JsonObject return when (json.string("command")) { "getInfo" -> { println("Getinfo Response: $response") mainResponseData = Klaxon().parse(response) // Call the next API call ws?.send( encrypt( json { obj("command" to "getTransactions") }.toJsonString() ) ) return ParseResponse() } "getTransactions" -> { transactions = json.array("transactions").orEmpty().map { tx -> TransactionItem( tx.string("type") ?: "", tx.long("datetime") ?: 0, tx.string("amount") ?: "0", tx.string("memo") ?: "", tx.string("address") ?: "", tx.string("txid") ?: "", tx.long("confirmations") ?: 0 ) } return ParseResponse(true) } "sendTx" -> { // Ignore return ParseResponse() } "sendTxSubmitted" -> { val txid = json.string("txid") return ParseResponse( false, "Tx submitted: $txid" ) } "sendTxFailed" -> { val err = json.string("err") return ParseResponse( false, "Tx displayMsg: $err" ) } else -> { Log.e(TAG, "Unknown command ${json.string("command")}") return ParseResponse() } } } fun setConnString(value: String?, context: Context) { val settings = context.getSharedPreferences("ConnInfo", 0) val editor = settings.edit() editor.putString("connstring", value) editor.apply() } fun getConnString(context: Context) : String? { val settings = context.getSharedPreferences("ConnInfo", 0) return settings.getString("connstring", null) } fun sendTx(tx: TransactionItem) { val payload = json { obj("command" to "sendTx", "tx" to obj( "amount" to tx.amount, "to" to tx.addr, "memo" to tx.memo )) } Log.w(TAG, payload.toJsonString(true)) ws?.send( encrypt( payload.toJsonString() ) ) } fun makeAPICalls() { if (getSecret() == null) { // Connected, but we don't have a secret, so we can't actually connect. ws?.close(1000, "No shared secret, can't connect") } else { // We make only the first API call here. The subsequent ones are made in parseResponsegit (), when this // call returns a reply val phoneName = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}" ws?.send( encrypt( json { obj("command" to "getInfo", "name" to phoneName) }.toJsonString() ) ) } } fun isValidAddress(a: String?) : Boolean { if (a == null) return false return if (isTestnet()) { Regex("^ztestsapling[a-z0-9]{76}", RegexOption.IGNORE_CASE).matches(a) || Regex("^tm[a-z0-9]{33}$", RegexOption.IGNORE_CASE).matches(a) } else { Regex("^zs1[a-z0-9]{75}$", RegexOption.IGNORE_CASE).matches(a) || Regex("^R[a-z0-9]{33}$", RegexOption.IGNORE_CASE).matches(a) } } fun isSaplingAddress(a: String?) : Boolean { return a?.startsWith("zs") ?: false || a?.startsWith("ztestsapling") ?: false } private fun decrypt(nonceHex: String, encHex: String) : String { // Enforce limits on sizes if (nonceHex.length > Sodium.crypto_secretbox_noncebytes() *2 || encHex.length > 2 * 50 * 1024 /*50kb*/) { return "error: Max size of message exceeded" } // First make sure the remote nonce is valid if (!checkRemoteNonce(nonceHex)) { return "error: Remote Nonce was too low" } val encsize = encHex.length / 2 val encbin = encHex.hexStringToByteArray(encHex.length / 2) val decrypted = ByteArray(encsize - Sodium.crypto_secretbox_macbytes()) val noncebin = nonceHex.hexStringToByteArray(Sodium.crypto_secretbox_noncebytes()) val result = Sodium.crypto_secretbox_open_easy(decrypted, encbin, encsize, noncebin, getSecret() ) if (result != 0) { return "error: Decryption Error" } Log.i(TAG, "Decrypted to: ${String(decrypted).replace("\n", " ")}") updateRemoteNonce(nonceHex) return String(decrypted) } private fun encrypt(s : String) : String { // Pad to 256 bytes, to prevent leaking any info via size of the encrypted message var inpStr = s if (inpStr.length % 256 > 0) { inpStr += " ".repeat(256 - (inpStr.length % 256)) } // Take the string, encrypt it and send it as the payload with the nonce in a Json string val msg = inpStr.toByteArray() check(getSecret() != null) val encrypted = ByteArray(msg.size + Sodium.crypto_secretbox_macbytes()) // Increment nonce val localNonce = incAndGetLocalNonce() val ret = Sodium.crypto_secretbox_easy(encrypted, msg, msg.size, localNonce, getSecret() ) if (ret != 0) { println("Encryption failed") } val j = json { obj("nonce" to localNonce.toHexString(), ("payload" to encrypted.toHexString()), ("to" to getWormholeCode()) )} println("Sending ${j.toJsonString()}") return j.toJsonString() } private fun checkRemoteNonce(remoteNonce: String): Boolean { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val prevNonceHex = settings.getString("remotenonce", "00".repeat(Sodium.crypto_secretbox_noncebytes()))!! // The problem is the nonces are hex encoded in little endian, but the BigDecimal contructor expects the nonces // in big endian format. So flip the endian-ness of the hex strings for comparision return BigInteger(remoteNonce.chunked(2).reversed().joinToString(""),16) > BigInteger(prevNonceHex.chunked(2).reversed().joinToString(""), 16) } private fun updateRemoteNonce(remoteNonce: String) { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val editor = settings.edit() editor.putString("remotenonce", remoteNonce) editor.apply() } private fun incAndGetLocalNonce() : ByteArray { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val nonceHex = settings.getString("localnonce", "00".repeat(Sodium.crypto_secretbox_noncebytes())) val nonce = nonceHex!!.hexStringToByteArray(Sodium.crypto_secretbox_noncebytes()) Sodium.sodium_increment(nonce, nonce.size) Sodium.sodium_increment(nonce, nonce.size) val editor = settings.edit() editor.putString("localnonce", nonce.toHexString()) editor.apply() return nonce } fun getWormholeCode() : String? { if (getSecret() == null) return null val tobin1 = ByteArray(Sodium.crypto_hash_sha256_bytes()) Sodium.crypto_hash_sha256(tobin1, getSecret(), getSecret()!!.size) val tobin2 = ByteArray(Sodium.crypto_hash_sha256_bytes()) Sodium.crypto_hash_sha256(tobin2, tobin1, tobin1.size) return tobin2.toHexString() } /* functions to set custom wormhole value from Settings tab */ fun setWormholeServer(customWormHole: String) { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val editor = settings.edit() editor.putString("wormhole", customWormHole) //editor.apply() editor.commit() } /* functions to get custom wormhole value */ fun getWormholeServer() : String? { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val wormHole = settings.getString("secret", "") if (wormHole.isNullOrEmpty()) { return settings.getString("wormhole", "https://wormhole.hush.is:443" ) } return settings.getString("wormhole", "" ) } fun setSecretHex(secretHex: String) { if (getSecret()?.toHexString() == secretHex) { return } val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val editor = settings.edit() editor.putString("secret", secretHex) editor.remove("localnonce") editor.remove("remotenonce") editor.apply() } fun getSecret() : ByteArray? { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val secretHex = settings.getString("secret", "") if (secretHex.isNullOrEmpty()) { return null } return secretHex.hexStringToByteArray(Sodium.crypto_secretbox_keybytes()) } fun setGlobalAllowInternet(allow: Boolean) { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val editor = settings.edit() editor.putBoolean("globalallowinternet", allow) editor.apply() } fun getGlobalAllowInternet(): Boolean { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) return settings.getBoolean("globalallowinternet", true) } fun setAllowInternet(allow: Boolean) { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) val editor = settings.edit() editor.putBoolean("allowinternet", allow) editor.apply() } fun getAllowInternet(): Boolean { val settings = SilentDragonApp.appContext!!.getSharedPreferences("Secret", 0) return settings.getBoolean("allowinternet", false) } private const val TAG = "DataModel" }