You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

408 lines
14 KiB

// Copyright 2019-2020 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<TransactionItem>? = null
val fee: Double = 0.0001
val currencyValues: HashMap<String, Double?> = HashMap()
val currencySymbols: HashMap<String, String> = 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<MainResponse>(response)
// Call the next API call
ws?.send(
encrypt(
json { obj("command" to "getTransactions") }.toJsonString()
)
)
return ParseResponse()
}
"getTransactions" -> {
transactions = json.array<JsonObject>("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.myhush.org: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"
}