2FA codes can now be scanned.
This commit is contained in:
@@ -1,25 +1,42 @@
|
||||
package com.github.mondei1.offpass
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.google.zxing.integration.android.IntentResult
|
||||
import dev.turingcomplete.kotlinonetimepassword.GoogleAuthenticator
|
||||
import kotlinx.android.synthetic.main.activity_create.*
|
||||
import kotlinx.coroutines.*
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class CreateActivity : AppCompatActivity() {
|
||||
private var fragment_title: TextInput? = null
|
||||
private var fragment_username: TextInput? = null
|
||||
private var schema: QRSchema? = null
|
||||
|
||||
private var fa_coroutine: Job? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
fragment_title = TextInput.newInstance("Title", "ENTER TITLE", "30dp")
|
||||
fragment_username = TextInput.newInstance("Username", "ENTER USERNAME", "30dp")
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.title_box, fragment_title!!)
|
||||
.replace(R.id.username, fragment_username!!)
|
||||
.commit()
|
||||
|
||||
//this.schema = QRSchema(this)
|
||||
//this.schema!!.decrypted_raw = "%JtuB4O9M42%Gitea|Nicolas|542superGoOD_pW&|klier.nicolas@protonmail.com|\$ul|(\$vb)\$O4|()What's your favorite series%Rick and morty|(2fa)otpauth://totp/OffPass%20Test?secret=d34gfkki5dkd5knifysrpgndd5xb2c7eddwki7ya4pvoisfa5c3ko5pv&issuer=Nicolas%20Klier"
|
||||
//this.schema!!.parse(this)
|
||||
@@ -34,6 +51,80 @@ class CreateActivity : AppCompatActivity() {
|
||||
Log.i("CREATE", "Back got clicked!")
|
||||
finish()
|
||||
}
|
||||
|
||||
fa_input.isFocusable = false
|
||||
|
||||
fa_input.setOnClickListener {
|
||||
if (fa_coroutine != null) {
|
||||
if (fa_coroutine!!.isActive) {
|
||||
// Copy code if already scanend
|
||||
Toast.makeText(this, "Copied!", Toast.LENGTH_SHORT).show()
|
||||
val clip: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clip.setPrimaryClip(ClipData.newPlainText("2FA code", fa_input.text))
|
||||
return@setOnClickListener
|
||||
}
|
||||
}
|
||||
|
||||
// Open QR-Code scanner
|
||||
IntentIntegrator(this)
|
||||
.setPrompt("Scan 2FA secret")
|
||||
.setBeepEnabled(false)
|
||||
.setBarcodeImageEnabled(false)
|
||||
.initiateScan()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat", "SetTextI18n")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
var result: IntentResult =
|
||||
IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
if (result.contents == null) {
|
||||
Toast.makeText(this, "Cancelled", Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
// We scanned a 2FA code
|
||||
if (result.contents.startsWith("otpauth://totp/")) {
|
||||
fa_progress.visibility = View.VISIBLE
|
||||
val fa_uri = Uri.parse(result.contents)
|
||||
val fa_generator = GoogleAuthenticator(base32secret = fa_uri.getQueryParameter("secret").toString())
|
||||
|
||||
/*val objAnim: ObjectAnimator = ObjectAnimator.ofInt(fa_progress, "progress")
|
||||
objAnim.duration = 300
|
||||
objAnim.interpolator = LinearInterpolator()
|
||||
objAnim.start()*/
|
||||
|
||||
// TODO: There is a bug. When the user changes his current app, this coroutine stops.
|
||||
fa_coroutine = GlobalScope.launch(Dispatchers.Main) {
|
||||
while (true) {
|
||||
fa_input.setText(fa_generator.generate())
|
||||
|
||||
var seconds_remaining = CryptoOperations().getRemainingTOTPTime()
|
||||
Log.i("2FA Generator", "Remaining: $seconds_remaining")
|
||||
|
||||
if (seconds_remaining == 0) seconds_remaining = 30
|
||||
|
||||
// Change color
|
||||
if (seconds_remaining >= 15) {
|
||||
fa_progress.progressTintList = ColorStateList.valueOf(Color.GREEN)
|
||||
} else if (seconds_remaining < 15 && seconds_remaining >= 8) {
|
||||
fa_progress.progressTintList = ColorStateList.valueOf(Color.YELLOW)
|
||||
} else {
|
||||
fa_progress.progressTintList = ColorStateList.valueOf(Color.RED)
|
||||
}
|
||||
|
||||
fa_progress.progress = seconds_remaining
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
fa_label.text = "${fa_uri.path?.replace("/", "")} (${fa_uri.getQueryParameter("issuer")})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
fa_coroutine?.cancel(CancellationException("Activity is stopping"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import android.util.Log
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.KeySpec
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.SecretKeyFactory
|
||||
@@ -27,13 +30,25 @@ class CryptoOperations {
|
||||
private val possibleChars: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
private lateinit var chars: CharArray
|
||||
|
||||
fun getRemainingTOTPTime(): Int {
|
||||
/*
|
||||
* If the time (in seconds) is 13, then this function should return 17.
|
||||
* If the time ("") is 46, then this function should return 14
|
||||
*/
|
||||
val format: DateFormat = SimpleDateFormat("ss")
|
||||
val time_seconds = format.format(Calendar.getInstance().time).toInt()
|
||||
|
||||
if (time_seconds <= 30) return 30 - time_seconds
|
||||
else return 60 - time_seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a password for storage.
|
||||
* Hashes a any input with
|
||||
*
|
||||
* @return a secure authentication token to be stored for later authentication
|
||||
*/
|
||||
fun hash(password: CharArray, salt: ByteArray): String {
|
||||
val spec: KeySpec = PBEKeySpec(password, salt, 85000, 8*31)
|
||||
val spec: KeySpec = PBEKeySpec(password, salt, 8000, 256)
|
||||
val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
val hash = factory.generateSecret(spec).encoded
|
||||
|
||||
@@ -41,6 +56,12 @@ class CryptoOperations {
|
||||
return Base64.encodeToString(hash, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function encrypts a provided string `plain` with a provided key `key`.
|
||||
* Encryption function is AES-256-CBC
|
||||
*
|
||||
* @return Encrypted plain in Base64 encoded (without wrapping)
|
||||
*/
|
||||
fun encrypt(key: String, plain: String): EncryptionResult {
|
||||
val salt = nextString(10).toByteArray()
|
||||
val iv = nextString(16).toByteArray()
|
||||
@@ -56,10 +77,10 @@ class CryptoOperations {
|
||||
}
|
||||
|
||||
val key: SecretKey = SecretKeySpec(aes_key, "AES")
|
||||
val cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
val cipher: Cipher = Cipher.getInstance("AES/CBC/ISO10126Padding")
|
||||
|
||||
// Performing actual crypto operation
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(128, iv))
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv))
|
||||
Log.i("Crypto", "Encrypt using ${String(aes_key)} '$plain' ${String(iv)}")
|
||||
val ciphered = cipher.doFinal(plain.toByteArray(Charsets.UTF_8))
|
||||
|
||||
@@ -67,6 +88,14 @@ class CryptoOperations {
|
||||
return EncryptionResult(Base64.encodeToString(ciphered, Base64.NO_WRAP), salt, iv)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function decrypts a provided string `encrypted` with it's iv `iv`, key `key` and
|
||||
* salt `salt`.
|
||||
*
|
||||
* Required is that the cipher AES-256-CBC was used.
|
||||
*
|
||||
* @return Decrypted cipher text as plain String.
|
||||
*/
|
||||
fun decrypt(encrypted: String, key: String, iv: ByteArray, salt: ByteArray): String {
|
||||
val key_bytes: ByteArray = hash(key.toCharArray(), salt).toByteArray(Charsets.UTF_8)
|
||||
val aes_key: ByteArray = ByteArray(32)
|
||||
@@ -84,10 +113,10 @@ class CryptoOperations {
|
||||
Log.i("Crypto", "Decrypt using ${String(aes_key)} '${Base64.encodeToString(encrypted_raw, Base64.NO_WRAP)} (${encrypted_raw.size})' ${String(iv)}")
|
||||
|
||||
val keySpec: SecretKey = SecretKeySpec(aes_key, "AES")
|
||||
val cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
val cipher: Cipher = Cipher.getInstance("AES/CBC/ISO10126Padding")
|
||||
|
||||
// Performing actual crypto operation
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, GCMParameterSpec(128, iv))
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, IvParameterSpec(iv))
|
||||
val ciphered = cipher.doFinal(encrypted_raw)
|
||||
|
||||
// Concat everything into one byte array
|
||||
@@ -98,6 +127,11 @@ class CryptoOperations {
|
||||
return String(byteBuffer.array())
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random IV used for encryption.
|
||||
*
|
||||
* @return Random IV of a length of 12 bytes
|
||||
*/
|
||||
fun nextIV(): ByteArray {
|
||||
val iv: ByteArray = ByteArray(12)
|
||||
val random: SecureRandom = SecureRandom()
|
||||
@@ -106,6 +140,9 @@ class CryptoOperations {
|
||||
return iv
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces secure random string. <b>This function can be slow, use only if necessary!</b>
|
||||
*/
|
||||
fun nextString(length: Int): String {
|
||||
chars = CharArray(length)
|
||||
for (idx in chars.indices) chars[idx] = possibleChars[random.nextInt(possibleChars.length)]
|
||||
@@ -114,17 +151,4 @@ class CryptoOperations {
|
||||
return String(chars)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CryptoEncryptionRunnable(
|
||||
private val key: String,
|
||||
private val data: String,
|
||||
private val iv: ByteArray
|
||||
) : HandlerThread("Enc_Crypto") {
|
||||
|
||||
override fun run() {
|
||||
Looper.prepare()
|
||||
val crypto = CryptoOperations()
|
||||
crypto.hash(key.toCharArray(), iv)
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ class MainActivity : AppCompatActivity() {
|
||||
.setBeepEnabled(false)
|
||||
.setBarcodeImageEnabled(false)
|
||||
.initiateScan()
|
||||
Log.i("Main", "Clicked on text")
|
||||
}
|
||||
|
||||
add_button.setOnClickListener {
|
||||
|
||||
@@ -32,7 +32,9 @@ class QRSchema {
|
||||
}
|
||||
|
||||
/**
|
||||
* This function takes a key `db_key` and tries to resolve it.
|
||||
* This function takes a key `key` and tries to resolve it.
|
||||
*
|
||||
* @return The decrypted value associated with the key.
|
||||
*/
|
||||
private suspend fun resolve(key: String, context: Context): String {
|
||||
val to_resolve = key.replace("$", "")
|
||||
@@ -53,6 +55,7 @@ class QRSchema {
|
||||
* This function will take the already set raw content and tries to parse it.
|
||||
*
|
||||
* @return It will return `true` if the operation was successful and `false` if otherwise.
|
||||
* Parsed objects can be read from this class instance.
|
||||
*/
|
||||
fun parse(context: Context): Boolean {
|
||||
if (decrypted_raw == "") {
|
||||
@@ -61,7 +64,7 @@ class QRSchema {
|
||||
}
|
||||
|
||||
// First will be to split the string into it's parts
|
||||
var fields: MutableList<String> = this.decrypted_raw.split("|").toMutableList()
|
||||
val fields: MutableList<String> = this.decrypted_raw.split("|").toMutableList()
|
||||
|
||||
for (i in 0 until fields.size) {
|
||||
// First four items have to exist and are title, username, password, URL in this order.
|
||||
@@ -83,7 +86,7 @@ class QRSchema {
|
||||
|
||||
// Look if website url is compressed
|
||||
if (this.website_url.startsWith("$")) {
|
||||
var that_class = this
|
||||
val that_class = this
|
||||
runBlocking {
|
||||
that_class.website_url = resolve(that_class.website_url, context)
|
||||
}
|
||||
@@ -111,7 +114,7 @@ class QRSchema {
|
||||
|
||||
// We got a security question/awnser
|
||||
if (key == "") {
|
||||
var qa = value.split("%")
|
||||
val qa = value.split("%")
|
||||
question_awnser.put(qa[0], qa[1]);
|
||||
} else {
|
||||
custom.put(key, value)
|
||||
@@ -131,7 +134,7 @@ class QRSchema {
|
||||
/**
|
||||
* This function will take an raw string input and will decrypt it.
|
||||
*
|
||||
* @return It returns the raw decrypted raw value.
|
||||
* @return It returns the decrypted raw value.
|
||||
*/
|
||||
fun decrypt(raw: String, passphrase: String): String {
|
||||
val parts = raw.split(":")
|
||||
|
||||
@@ -44,6 +44,7 @@ class TextInput : Fragment() {
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
head.text = arg_head
|
||||
title_.hint = arg_title
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
Reference in New Issue
Block a user