diff --git a/.idea/gradle.xml b/.idea/gradle.xml index be83284ab3..43336cbab3 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -17,6 +17,7 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 77eb034d59..83fc66e693 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -133,6 +133,8 @@ dependencies { debugImplementation(Dependencies.ThirdParty.whatthestack) } + implementation(Dependencies.ThirdParty.androidasync) + "nonFreeImplementation"(Dependencies.NonFree.google_play_auth_api_phone) // Testing-only dependencies diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt index ffbe5bc819..9254e96746 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt @@ -69,13 +69,18 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) { super.onViewCreated(view, savedInstanceState) settings = requireContext().sharedPrefs initializePasswordList() + binding.fab.setOnClickListener { ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET") } + binding.buttonOopass.setOnClickListener{ + ItemCreationBottomSheet.newInstance("oopass").show(childFragmentManager, "BOTTOM_SHEET") + } childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) { _, bundle -> when (bundle.getString(ACTION_KEY)) { ACTION_FOLDER -> requireStore().createFolder() ACTION_PASSWORD -> requireStore().createPassword() + ACTION_OOPASS -> requireStore().getOopassMain(bundle.getString("_master_password").toString(), bundle.getString("_auth_user").toString(), bundle.getString("_auth_domain").toString()) } } } @@ -313,6 +318,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) { const val ACTION_KEY = "action" const val ACTION_FOLDER = "folder" const val ACTION_PASSWORD = "password" + const val ACTION_OOPASS = "oopass" fun newInstance(args: Bundle): PasswordFragment { val fragment = PasswordFragment() diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index b0f8602cd2..a95fa7b728 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -66,6 +66,31 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git +import com.koushikdutta.async.http.AsyncHttpClient +import com.koushikdutta.async.http.AsyncHttpClient.WebSocketConnectCallback +import com.koushikdutta.async.http.WebSocket +import com.koushikdutta.async.http.WebSocket.StringCallback +import com.koushikdutta.async.callback.DataCallback +import com.koushikdutta.async.callback.CompletedCallback +import com.koushikdutta.async.DataEmitter +import com.koushikdutta.async.ByteBufferList +import com.zeapo.pwdstore.crypto.ARC4 +import com.zeapo.pwdstore.crypto.prng +import com.zeapo.pwdstore.crypto.seedrandom +import java.math.BigInteger +import java.security.SecureRandom +import java.security.MessageDigest +import java.util.Random +import javax.crypto.Mac +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import kotlin.math.floor +import kotlin.math.roundToInt +import org.bouncycastle.asn1.sec.SECNamedCurves +import org.bouncycastle.asn1.x9.X9ECParameters +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.math.ec.ECPoint +import org.bouncycastle.util.encoders.Hex const val PASSWORD_FRAGMENT_TAG = "PasswordsList" @@ -501,6 +526,368 @@ class PasswordStore : BaseGitActivity() { FolderCreationDialogFragment.newInstance(currentDir.path).show(supportFragmentManager, null) } + fun getOopassMain(_master_password:String, _auth_user:String, _auth_domain:String) { + if(!validateState()) return + + val _setting_api_key_email:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_API_KEY_EMAIL) + val _setting_server_host:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_SERVER_HOST) + val _setting_server_port:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_SERVER_PORT) + val _setting_requested_chars:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_REQUESTED_CHARS) + val _setting_requested_length:Int = sharedPrefs.getString(PreferenceKeys.OOPASS_REQUESTED_LENGTH)?.toIntOrNull() ?: 16 + val _setting_hmac_key:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_HMAC_KEY) + + // initialize hmac key if not yet set for this device + if ( _setting_hmac_key == null || _setting_hmac_key.trim().length == 0 ) + { + val random_seed:SecureRandom = SecureRandom() + val random_bytes:ByteArray = ByteArray(4) + random_seed.nextBytes(random_bytes) + + val random_md5:MessageDigest = MessageDigest.getInstance("MD5") + + val random_32bytes:ByteArray = random_md5.digest( random_bytes ) + + sharedPrefs.edit { putString(PreferenceKeys.OOPASS_HMAC_KEY, String(Hex.encode(random_32bytes))) } + + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle("Notice: HMAC Key Generated") + .setMessage("An HMAC key was not yet set for your device and has been generated.\n\nYou may go to the settings to set another value for the HMAC key.") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + if ( _setting_api_key_email == null || _setting_api_key_email.trim().length == 0 ) + { + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle("Error: Missing API Email") + .setMessage("Go to the settings to set a value for the API email") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + + return + } + + if ( _setting_server_host == null || _setting_server_host.trim().length == 0 ) + { + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle("Error: Missing Server Host") + .setMessage("Go to the settings to set a value for the server host") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + + return + } + + if ( _setting_server_port == null || _setting_server_port.trim().length == 0 ) + { + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle("Error: Missing Server Port") + .setMessage("Go to the settings to set a value for the server port") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + + return + } + + if ( _setting_requested_chars == null || _setting_requested_chars.trim().length == 0 ) + { + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle("Error: Missing Desired Characters") + .setMessage("Go to the settings to set a value for the desired password characters") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + + return + } + + if ( _setting_requested_length < 1 ) + { + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle("Error: Invalid Desired Length") + .setMessage("Go to the settings to set a value for the desired password length") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + + return + } + + val hmac_options_key:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_HMAC_KEY) + val hmac_options_algorithm:String = "HmacSHA256" + val oprf_hmac_sha256_key:SecretKey = SecretKeySpec(hmac_options_key?.toByteArray(), hmac_options_algorithm) + val ec_options:X9ECParameters = SECNamedCurves.getByName("secp256k1") + + var randomRho:BigInteger = BigInteger.ONE + + val client_version:String = "1.1.2.android" + val protocol_version:String = "2.0.*" + var _socket:WebSocket? = null + + val _auth_offset:Int = 0 + + val handle_socket_connect = object:WebSocketConnectCallback { + override fun onCompleted(ex:Exception?, webSocket:WebSocket) { + ex?.printStackTrace() + return + } + } + + val handle_socket_closed_ended = object:CompletedCallback { + override fun onCompleted(ex:Exception?) { + ex?.printStackTrace() + return + } + } + + val handle_socket_data_read = object:DataCallback { + override fun onDataAvailable(emitter:DataEmitter, byteBufferList:ByteBufferList) { + //println("onDataAvailable()") + + // note that this data has been read + byteBufferList.recycle() + } + } + + val handle_socket_received_beta = object:StringCallback { + override fun onStringAvailable(s: String) { + val data_array:List = s.split(",") + + if ( data_array.size.equals(2) ) + { + val beta_x_str:String = data_array.get(0) + val beta_y_str:String = data_array.get(1) + + println("RECEIVED CURVE POINTS X,Y:'" + beta_x_str + "','" + beta_y_str + "'") + + val beta_x:BigInteger = BigInteger(1, Hex.decodeStrict(beta_x_str)) + val beta_y:BigInteger = BigInteger(1, Hex.decodeStrict(beta_y_str)) + + val beta_point:ECPoint = ec_options.getCurve().createPoint(beta_x, beta_y) + + println("beta_point.affineX:Y::'" + beta_point.normalize().getXCoord().toString() + "':'" + beta_point.normalize().getYCoord().toString() + "'") + + val is_beta_point_member:Boolean = beta_point.isValid() + + if ( is_beta_point_member ) + { + println("Point is a member of curve") + + // CANCEL OUT RANDOM LARGE VALUE IN BETA THAT WAS USED TO BLIND ALPHA + + // aka: rwd = H'(x)^k = (beta)^(1/rho) + val rho_inv:BigInteger = randomRho.modInverse( ec_options.getN() ) + val rwd_point:ECPoint = beta_point.multiply( rho_inv ) + + // GENERATE HMAC SHA256 OF RESULTING X,Y PAIR ON CURVE AND MASTER PASSWORD AS RESULT OF OPRF EXCHANGE + + //val rwd_x:ByteArray = rwd_point.normalize().getXCoord().toBigInteger().toByteArray() + //val rwd_y:ByteArray = rwd_point.normalize().getYCoord().toBigInteger().toByteArray() + val rwd_x:String = rwd_point.normalize().getXCoord().toString().padStart(64, '0') + val rwd_y:String = rwd_point.normalize().getYCoord().toString().padStart(64, '0') + + //println("rwd_point.affineX:Y::'" + String(Hex.encode(rwd_x)) + "':'" + String(Hex.encode(rwd_y)) + "'") + println("rwd_point.affineX:Y::'" + rwd_x + "':'" + rwd_y + "'") + println("rwd_point.isOnCurve():'" + rwd_point.isValid().toString()) + + //val rwd:String = String(Hex.encode(rwd_x)) + String(Hex.encode(rwd_y)) + _master_password + val rwd:String = rwd_x + rwd_y + _master_password + + val rwd_hmac_sha256_key:SecretKey = SecretKeySpec(_auth_domain.toByteArray(), hmac_options_algorithm) + val rwd_hmac_sha256:Mac = Mac.getInstance(hmac_options_algorithm, BouncyCastleProvider()) + + rwd_hmac_sha256.init(rwd_hmac_sha256_key) + rwd_hmac_sha256.update(rwd.toByteArray()) + + val rwd_hmac_sha256_encrypted:ByteArray = rwd_hmac_sha256.doFinal() + val hashed:String = String(Hex.encode(rwd_hmac_sha256_encrypted)) + + println("Resolved Beta: '" + hashed + "'") + + // MAP RESULT TO CONFIGURABLE PASSWORD ALPHABET + var seedrandom: ARC4 = seedrandom(hashed) + + var pass:String = "" + + for (i:Int in 0..(_setting_requested_length-1)) + { + //println(prng(seedrandom)) + pass += _setting_requested_chars.get(floor(prng(seedrandom) * _setting_requested_chars.length).roundToInt()) + } + + // SAVE TO CLIPBOARD WITH TIMER TO CLEAR + val decryptIntent = Intent(this@PasswordStore, DecryptActivity::class.java) + decryptIntent.putExtra("NAME", "OOPASS: Copied to Clipboard") + decryptIntent.putExtra("FILE_PATH", "OOPASS: Copied to Clipboard") + decryptIntent.putExtra("REPO_PATH", "OOPASS") + decryptIntent.putExtra("LAST_CHANGED_TIMESTAMP", "") + decryptIntent.putExtra("OOPASS_DATA", pass) + startActivity(decryptIntent) + + //runOnUiThread(object:Runnable{ + // override fun run() { + // //(BasePgpActivity::copyPasswordToClipboard)(BasePgpActivity(), pass) + // (DecryptActivity::copyPasswordToClipboard)(DecryptActivity(), pass) + // //MaterialAlertDialogBuilder(this@PasswordStore) + // // .setTitle("Your Password") + // // .setMessage(pass) + // // //.setCancelable(true) + // // .setPositiveButton(android.R.string.ok, null) + // // .show() + // } + //}) + } + else + { + println("Point is NOT a member of curve") + } + } + else if ( s.equals("invalid") ) + { + println("Point SENT is NOT a member of curve") + } + + println("Closing socket connection") + _socket?.close() + + return + } + } + + val handle_socket_data = object:StringCallback { + override fun onStringAvailable(s:String) { + //println("onStringAvailable(): '" + s + "'") + + if ( s.equals("__protocol_" + protocol_version + "_connected__") ) + { + // replace string callback handler + _socket?.setStringCallback(handle_socket_received_beta) + + // GENERATE USER HASH TO BE BLINDED AND SENT TO SERVER + + //generate hash( username+domain+ctr+password ) + var hashForOPRF:String = _auth_user + _auth_domain + _auth_offset + _master_password + + val oprf_hmac_sha256:Mac = Mac.getInstance(hmac_options_algorithm, BouncyCastleProvider()) + + oprf_hmac_sha256.init(oprf_hmac_sha256_key) + oprf_hmac_sha256.update(hashForOPRF.toByteArray()) + + val oprf_hmac_sha256_encrypted:ByteArray = oprf_hmac_sha256.doFinal() + hashForOPRF = String(Hex.encode(oprf_hmac_sha256_encrypted)) + + // GENERATE RANDOM LARGE VALUE FOR USE IN BLINDING OUR USER HASH + + //ensure binary length >= (256+80=336 bits) 42bytes of entropy or more + val random_seed:SecureRandom = SecureRandom() + val random_bytes:ByteArray = ByteArray(43) + random_seed.nextBytes(random_bytes) + + randomRho = BigInteger(random_bytes) + + println("randomRho:bitlength::'" + randomRho + "':'" + randomRho.bitLength() + "'") + + // GENERATE BLINDED ALPHA + + // generate alpha = (hashForAlpha)^randomRho + // aka: alpha=H'(x)^rho + // power represented as point multiplication on the curve + + val hash_bigi:BigInteger = BigInteger(1, Hex.decodeStrict(hashForOPRF)) + + println("hashForOPRF:bigi::'" + hashForOPRF + "':'" + hash_bigi + "'") + + // get x,y pair on curve using user hmac as X + // Bitcoin keys use the secp256k1 (info on 2.7.1) parameters. + // Public keys are generated by: Q=dG where Q is the public key, d is the private key, and G is a curve parameter. + // A public key is a 65 byte long value consisting of a leading 0x04 and X and Y coordinates of 32 bytes each. + // http://www.secg.org/collateral/sec2_final.pdf + // aka: Q = alpha_point; d = hash_bigi; G = ec_options.G + + val alpha_point:ECPoint = ec_options.getG().multiply( hash_bigi ) + //println(ec_options.getG().toString()) + //println(ec_options.getG().normalize().affineXCoord.toString()) + //println(ec_options.getG().normalize().affineYCoord.toString()) + //println(ec_options.getG().normalize().rawXCoord.toString()) + //println(ec_options.getG().normalize().rawYCoord.toString()) + //println(ec_options.getG().normalize().xCoord.toString()) + //println(ec_options.getG().normalize().yCoord.toString()) + println("alpha_point.affineX:Y::'" + alpha_point.normalize().getXCoord().toString() + "':'" + alpha_point.normalize().getYCoord().toString() + "'") + println("alpha_point.isOnCurve():'" + alpha_point.isValid().toString()) + + // point multiply with random large value, assumed reduction by ec_options.p + val alpha_mult:ECPoint = alpha_point.multiply( randomRho ) + //val testr:BigInteger = BigInteger("7830433108338161311672534626810729051862579494468904564511192911136080794165799472284225839193527826330") + //println("testr:bitlength::'" + testr + "':'" + testr.bitLength() + "'") + //randomRho = testr + //val alpha_mult:ECPoint = alpha_point.multiply( testr ) + + // get x,y pair after point multiplication as 32byte==256bit length strings + //val alpha_x:ByteArray = alpha_mult.normalize().getXCoord().toBigInteger().toByteArray() + //val alpha_y:ByteArray = alpha_mult.normalize().getYCoord().toBigInteger().toByteArray() + val alpha_x:String = alpha_mult.normalize().getXCoord().toString().padStart(64, '0') + val alpha_y:String = alpha_mult.normalize().getYCoord().toString().padStart(64, '0') + + //println("alpha_mult.affineX:Y::'" + String(Hex.encode(alpha_x)) + "':'" + String(Hex.encode(alpha_y)) + "'") + println("alpha_mult.affineX:Y::'" + alpha_mult.normalize().getXCoord().toString() + "':'" + alpha_mult.normalize().getYCoord().toString() + "'") + println("alpha_mult.isOnCurve():'" + alpha_mult.isValid().toString()) + + // will send x,y pair to server for decoding + //val alpha:String = String(Hex.encode(alpha_x)) + "," + String(Hex.encode(alpha_y)) + val alpha:String = alpha_x + "," + alpha_y + + // GENERATE UNIQUE USER ID TO ASSOCIATE EMAIL FOR GEOIP VIOLATION NOTIFICATIONS + + var user_identifier:String = _auth_user + _auth_domain + + // keyed hash of user_identifier into hex string + val user_identifier_hmac_sha256_key:SecretKey = SecretKeySpec(_auth_offset.toString().toByteArray(), hmac_options_algorithm) + val user_identifier_hmac_sha256:Mac = Mac.getInstance(hmac_options_algorithm, BouncyCastleProvider()) + + user_identifier_hmac_sha256.init(user_identifier_hmac_sha256_key) + user_identifier_hmac_sha256.update(user_identifier.toByteArray()) + + val user_identifier_hmac_sha256_encrypted:ByteArray = user_identifier_hmac_sha256.doFinal() + user_identifier = String(Hex.encode(user_identifier_hmac_sha256_encrypted)) + + // SEND VALUES TO SERVER + + //val _setting_api_key_email:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_API_KEY_EMAIL) + + println("Sending Alpha + User Identifier + User Email") + println("Alpha:'" + alpha + "'::User Identifier:'" + user_identifier + "'::User Email:'" + _setting_api_key_email + "'") + + val request_data:String = alpha + "," + user_identifier + "," + _setting_api_key_email + + _socket?.send(request_data) + } + + return + } + } + + //val _setting_server_host:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_SERVER_HOST) + //val _setting_server_port:String? = sharedPrefs.getString(PreferenceKeys.OOPASS_SERVER_PORT) + val _server_protocol:String = "wss" + + val _socket_url:String? = _server_protocol + "://" + _setting_server_host + ":" + _setting_server_port + + val wsFuture = AsyncHttpClient.getDefaultInstance().websocket(_socket_url, _server_protocol, handle_socket_connect) + _socket = wsFuture.get() + + _socket.setClosedCallback(handle_socket_closed_ended) + _socket.setEndCallback(handle_socket_closed_ended) + _socket.setDataCallback(handle_socket_data_read) + _socket.setStringCallback(handle_socket_data) + + // handle_socket_opened + _socket.send("__client_" + client_version + "_connected__") + } + fun deletePasswords(selectedItems: List) { var size = 0 selectedItems.forEach { diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/ARC4.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/ARC4.kt new file mode 100644 index 0000000000..0f580187e5 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/ARC4.kt @@ -0,0 +1,265 @@ +package com.zeapo.pwdstore.crypto + +import kotlin.math.* +import java.math.BigInteger + +// ADAPTED FROM https://github.com/davidbau/seedrandom/blob/released/seedrandom.js + +/* + shl - signed shift left (equivalent of << operator) + shr - signed shift right (equivalent of >> operator) + ushr - unsigned shift right (equivalent of >>> operator) + and - bitwise and (equivalent of & operator) + or - bitwise or (equivalent of | operator) + xor - bitwise xor (equivalent of ^ operator) + inv - bitwise complement (equivalent of ~ operator) +*/ + +fun getMutListEl(ml:MutableList, i:Int): Int { + try { + return ml[i] + } catch (e:IndexOutOfBoundsException) { + //println("getMutListEl(): " + e) + return 0 + } +} + +fun setMutListEl(ml:MutableList, i:Int, v:Int) { + try { + ml[i] = v + } catch (e:IndexOutOfBoundsException) { + //println("setMutListEl(): " + e) + ml.add(i, v) + } +} + +var pool:MutableList = mutableListOf() + +var width:Int = 256 // each RC4 output is 0 <= x < 256 +var chunks:Int = 6 // at least six RC4 outputs for each double +var digits:Int = 52 // there are 52 significant digits in a double +var rngname:String = "random" // rngname: name for Math.random and Math.seedrandom +var startdenom:Long = (width*1.0).pow(chunks).toLong() +var significance:Long = (2.0).pow(digits).toLong() +var overflow:Long = significance * 2 +var mask:Int = width - 1 + +// +// seedrandom() +// This is the seedrandom function described above. +// +fun seedrandom(seed:String): ARC4 { + var key:MutableList = mutableListOf() + + var shortseed:String = mixkey(seed, key) + + //println(key) + //println(shortseed) + + // Use the seed to initialize an ARC4 generator. + var arc4:ARC4 = ARC4(key) + + //println("back in main()") + //println(arc4.S) //102,179,90,72,191,58,6,1,228,245,... + //println(arc4.i) //0 + //println(arc4.j) //163 + + // Mix the randomness into accumulated entropy. + mixkey(tostring(arc4.S), pool) + + //return prng(arc4) + return arc4 +} + +// This function returns a random double in [0, 1) that contains +// randomness in every bit of the mantissa of the IEEE 754 value. +fun prng(arc4:ARC4): Double { + var n:Long = arc4.g(chunks).toLong() + var d:BigInteger = startdenom.toBigInteger() + var x:Long = 0 + //println("prng()") + while (n < significance) { + //println("n:" + n + ",d:" + d + ",x:" + x) + n = (n + x) * width + d = d * width.toBigInteger() + x = arc4.g(1).toLong() + } + //println("finish (n < significance)") + //println("n:" + n + ",d:" + d + ",x:" + x) + while (n >= overflow) { + //println("n:" + n + ",d:" + d + ",x:" + x) + n = n / 2 + d = d / 2.toBigInteger() + x = x ushr 1 + } + //println("finish (n >= overflow)") + //println("n:" + n + ",d:" + d + ",x:" + x) + + var _temp_top:BigInteger = (n + x).toBigInteger() + + return (_temp_top.toDouble() / d.toDouble()) + //return (n + x) / (d * 1.0) +} + +fun prng_int32(arc4:ARC4): Long { + return arc4.g(4) or 0 +} + +fun prng_quick(arc4:ARC4): Long { + return arc4.g(4) / 0x100000000 +} + +fun prng_double(arc4:ARC4): Double { + return prng(arc4) +} + +// +// ARC4 +// +// An ARC4 implementation. The constructor takes a key in the form of +// an array of at most (width) integers that should be 0 <= x < (width). +// +// The g(count) method returns a pseudorandom integer that concatenates +// the next (count) outputs from ARC4. Its return value is a number x +// that is in the range 0 <= x < (width ^ count). +// +class ARC4 { + var i:Int + var j:Int + var S:MutableList + + init { + //println("init ARC4") + + i = 0 + j = 0 + S = mutableListOf() + } + + constructor(arg_key:MutableList) { + var key:MutableList = arg_key + + //println("ARC4:construct") + + var t:Int = 0 + var keylen:Int = key.size + var i:Int = 0 + var j:Int = 0 + var s:MutableList = this.S + + // The empty key [] is treated as [0]. + if (keylen == 0) { + key = mutableListOf() + + //key[0] = 0 + setMutListEl(key, 0, 0) + + keylen += 1 + } + + // Set up S using the standard key scheduling algorithm. + while (i < width) { + //s[i] = i++ + setMutListEl(s, i, i++) + } + + for (iterator in 0..(width-1)) { + //t = s[i] + t = getMutListEl(s, iterator) + + //j = mask and (j + key[i % keylen] + t) + j = mask and (j + getMutListEl(key, iterator % keylen) + t) + + //s[i] = s[j] + setMutListEl(s, iterator, getMutListEl(s, j)) + + //s[j] = t + setMutListEl(s, j, t) + } + + //println(key) //[114, 64, 110, 100, 48, 109, 107, 51, 121] + //println(t) //28 + //println(keylen) //9 + //println(i) //256 + //println(j) //29 + //println(s) //[ 46, 188, 45, 73, 190, 52, 161, 29, 92, 17, … + //println(this.S) //S:[ 46, 188, 45, 73, 190, 52, 161, 29, 92, 17, … + //println(this.i) //i:0 + //println(this.j) //j:0 + + g(width) + } + + // The "g" method returns the next (count) outputs as one number. + fun g(arg_count:Int): Long { + var count:Int = arg_count + + // Using instance members instead of closure state nearly doubles speed. + var t:Int = 0 + var r:Long = 0 + var i:Int = this.i + var j:Int = this.j + var s:MutableList = this.S + + while (count != 0) { + count -= 1 + + i = mask and (i + 1) + + //t = s[i] + t = getMutListEl(s, i) + + j = mask and (j + t) + + //s[i] = s[j] + setMutListEl(s, i, getMutListEl(s, j)) + + //s[j] = t + setMutListEl(s, j, t) + + //r = r * width + s[mask and (s[i] + s[j])] + r = r * width + getMutListEl(s, mask and (getMutListEl(s, i) + getMutListEl(s, j))) + } + + this.i = i + this.j = j + + return r + // For robust unpredictability, the function call below automatically + // discards an initial batch of values. This is called RC4-drop[256]. + // See http://google.com/search?q=rsa+fluhrer+response&btnI + } +} + +// +// mixkey() +// Mixes a string seed into a key that is an array of integers, and +// returns a shortened string seed that is equivalent to the result key. +// +fun mixkey(seed:String, key:MutableList): String { + var stringseed:String = seed + "" + var smear:Int = 0 + var j:Int = 0 + + while (j < stringseed.length) { + //smear = smear xor key[mask and j] * 19 + smear = smear xor getMutListEl(key, mask and j) * 19 + + //key[mask and j] = mask and (smear + stringseed.get(j).toInt()) + setMutListEl(key, mask and j, mask and (smear + stringseed.get(j).toInt())) + + j += 1 + } + + //return String(key.toIntArray(), 0, key.size) + return tostring(key) +} + +// +// tostring() +// Converts an array of charcodes to a string +// +fun tostring(a:MutableList): String { + return String(a.toIntArray(), 0, a.size) +} + diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt index 6b6c2032fb..432581faae 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt @@ -52,6 +52,11 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou */ val fullPath: String by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra("FILE_PATH") } + /** + * OOPASS Data for clipboard + */ + val oopassData: String by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra("OOPASS_DATA") } + /** * Name of the password file * diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt index cfcecc2240..e5e9507496 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt @@ -153,6 +153,10 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { @OptIn(ExperimentalTime::class) private fun decryptAndVerify(receivedIntent: Intent? = null) { + if (this.repoPath.equals("OOPASS")) { + copyPasswordToClipboard(this.oopassData) + return + } if (api == null) { bindToOpenKeychain(this) return diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/ItemCreationBottomSheet.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/ItemCreationBottomSheet.kt index 5acc80916d..76f2a4e84c 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/ItemCreationBottomSheet.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/ItemCreationBottomSheet.kt @@ -10,21 +10,34 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver +import android.widget.EditText import android.widget.FrameLayout import androidx.core.os.bundleOf import androidx.fragment.app.setFragmentResult import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.PasswordFragment.Companion.ACTION_FOLDER import com.zeapo.pwdstore.PasswordFragment.Companion.ACTION_KEY import com.zeapo.pwdstore.PasswordFragment.Companion.ACTION_PASSWORD +import com.zeapo.pwdstore.PasswordFragment.Companion.ACTION_OOPASS import com.zeapo.pwdstore.PasswordFragment.Companion.ITEM_CREATION_REQUEST_KEY import com.zeapo.pwdstore.R import com.zeapo.pwdstore.utils.resolveAttribute class ItemCreationBottomSheet : BottomSheetDialogFragment() { + companion object { + @JvmStatic + fun newInstance(type: String) = ItemCreationBottomSheet().apply { + arguments = Bundle().apply { + putString("type", type) + } + } + } + + private var type: String = "" private var behavior: BottomSheetBehavior? = null private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() { override fun onSlide(bottomSheet: View, slideOffset: Float) { @@ -39,7 +52,20 @@ class ItemCreationBottomSheet : BottomSheetDialogFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { if (savedInstanceState != null) dismiss() - return inflater.inflate(R.layout.item_create_sheet, container, false) + arguments?.getString("type")?.let { + type = it + } + + //val builder = android.app.AlertDialog.Builder(context) + //builder.setMessage(type) + //builder.create() + //builder.show() + + if (type.equals("oopass")) { + return inflater.inflate(R.layout.oopass_sheet, container, false) + } else { + return inflater.inflate(R.layout.item_create_sheet, container, false) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -62,6 +88,46 @@ class ItemCreationBottomSheet : BottomSheetDialogFragment() { setFragmentResult(ITEM_CREATION_REQUEST_KEY, bundleOf(ACTION_KEY to ACTION_PASSWORD)) dismiss() } + dialog.findViewById(R.id.oopass_get_password)?.setOnClickListener { + val _master_password:String = (view.findViewById(R.id.oopass_mp) as EditText).getText().toString() + val _auth_user = (view.findViewById(R.id.oopass_user) as EditText).getText().toString() + val _auth_domain = (view.findViewById(R.id.oopass_location) as EditText).getText().toString() + + if ( _master_password.length < 6 ) { + context?.let { contextConditional -> + MaterialAlertDialogBuilder(contextConditional) + .setTitle("Error") + .setMessage("Password must be a minimum of 6 characters") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + else if ( _auth_user.length < 1 ) { + context?.let { contextConditional -> + MaterialAlertDialogBuilder(contextConditional) + .setTitle("Error") + .setMessage("Location must be a minimum of 1 character") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + else if ( _auth_domain.length < 1 ) { + context?.let { contextConditional -> + MaterialAlertDialogBuilder(contextConditional) + .setTitle("Error") + .setMessage("Service must be a minimum of 1 character") + //.setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + else { + setFragmentResult(ITEM_CREATION_REQUEST_KEY, bundleOf(ACTION_KEY to ACTION_OOPASS, "_master_password" to _master_password, "_auth_user" to _auth_user, "_auth_domain" to _auth_domain)) + dismiss() + } + } } }) val gradientDrawable = GradientDrawable().apply { diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt index 5ec4063904..f09f4159bb 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt @@ -86,4 +86,11 @@ object PreferenceKeys { const val PROXY_PORT = "proxy_port" const val PROXY_USERNAME = "proxy_username" const val PROXY_PASSWORD = "proxy_password" + + const val OOPASS_API_KEY_EMAIL = "oopass_api_key_email" + const val OOPASS_SERVER_HOST = "oopass_server_host" + const val OOPASS_SERVER_PORT = "oopass_server_port" + const val OOPASS_REQUESTED_CHARS = "oopass_requested_chars" + const val OOPASS_REQUESTED_LENGTH = "oopass_requested_length" + const val OOPASS_HMAC_KEY = "oopass_hmac_key" } diff --git a/app/src/main/res/layout/oopass_sheet.xml b/app/src/main/res/layout/oopass_sheet.xml new file mode 100644 index 0000000000..b347f57f27 --- /dev/null +++ b/app/src/main/res/layout/oopass_sheet.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/password_recycler_view.xml b/app/src/main/res/layout/password_recycler_view.xml index 8358ed997e..150bd5108d 100644 --- a/app/src/main/res/layout/password_recycler_view.xml +++ b/app/src/main/res/layout/password_recycler_view.xml @@ -38,4 +38,13 @@ android:src="@drawable/ic_add_48dp" app:backgroundTint="?attr/colorSecondary" app:rippleColor="?attr/colorSecondary" /> + +