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" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 885753e948..e6b56667d2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -156,6 +156,19 @@
Export passwords
Exports the encrypted passwords to an external directory
Version
+ OOPASS
+ Email for API
+ Email for API abuse notifications
+ Server (HOST/IP)
+ OOPASS server hostname or IP address
+ Server Port
+ OOPASS server port
+ Desired Password Characters
+ Possible characters used in all generated passwords
+ Desired Password Length
+ Length of all generated passwords
+ HMAC key
+ Change to migrate between devices: 32 bytes alphanumeric
Generate Password
@@ -336,6 +349,15 @@
Wrong password
Create new folder
Create new password
+ Provide the information and tap "Get Password"
+ OOPASS Password Manager
+ Get Password
+ Enter Master Password:
+ Type here
+ Enter Location Phrase:
+ gmail.com
+ Enter Service User:
+ username
Grant
Enable debug logging (requires app restart)
Debug logging
diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml
index 58a173c52a..8b30e79cae 100644
--- a/app/src/main/res/xml/preference.xml
+++ b/app/src/main/res/xml/preference.xml
@@ -164,6 +164,39 @@
app:title="@string/pref_copy_title" />
+
+
+
+
+
+
+
+
+