diff --git a/app/build.gradle b/app/build.gradle index 3415a72..c68f3e9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,6 +42,10 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.0' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7" + + / One-time password / + implementation "dev.turingcomplete:kotlin-onetimepassword:2.0.1" / QRCode reader / implementation 'com.journeyapps:zxing-android-embedded:4.1.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc4c495..858d190 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ = 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")) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt b/app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt index aa682b1..628e85f 100644 --- a/app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt +++ b/app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt @@ -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. This function can be slow, use only if necessary! + */ 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) - } } \ No newline at end of file diff --git a/app/src/main/java/com/github/mondei1/offpass/MainActivity.kt b/app/src/main/java/com/github/mondei1/offpass/MainActivity.kt index 727fc98..0b615ef 100644 --- a/app/src/main/java/com/github/mondei1/offpass/MainActivity.kt +++ b/app/src/main/java/com/github/mondei1/offpass/MainActivity.kt @@ -46,7 +46,6 @@ class MainActivity : AppCompatActivity() { .setBeepEnabled(false) .setBarcodeImageEnabled(false) .initiateScan() - Log.i("Main", "Clicked on text") } add_button.setOnClickListener { diff --git a/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt b/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt index 7929c21..5391a06 100644 --- a/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt +++ b/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt @@ -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 = this.decrypted_raw.split("|").toMutableList() + val fields: MutableList = 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(":") diff --git a/app/src/main/java/com/github/mondei1/offpass/TextInput.kt b/app/src/main/java/com/github/mondei1/offpass/TextInput.kt index dff8d15..e2dfbb5 100644 --- a/app/src/main/java/com/github/mondei1/offpass/TextInput.kt +++ b/app/src/main/java/com/github/mondei1/offpass/TextInput.kt @@ -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 { diff --git a/app/src/main/res/drawable/dummy_qr_code.png b/app/src/main/res/drawable/dummy_qr_code.png new file mode 100644 index 0000000..6fca785 Binary files /dev/null and b/app/src/main/res/drawable/dummy_qr_code.png differ diff --git a/app/src/main/res/font/robotobold.ttf b/app/src/main/res/font/robotobold.ttf new file mode 100644 index 0000000..d998cf5 Binary files /dev/null and b/app/src/main/res/font/robotobold.ttf differ diff --git a/app/src/main/res/font/robotoregular.ttf b/app/src/main/res/font/robotoregular.ttf new file mode 100644 index 0000000..2b6392f Binary files /dev/null and b/app/src/main/res/font/robotoregular.ttf differ diff --git a/app/src/main/res/font/robotothin.ttf b/app/src/main/res/font/robotothin.ttf new file mode 100644 index 0000000..4e797cf Binary files /dev/null and b/app/src/main/res/font/robotothin.ttf differ diff --git a/app/src/main/res/layout/activity_create.xml b/app/src/main/res/layout/activity_create.xml index 4929739..1183075 100644 --- a/app/src/main/res/layout/activity_create.xml +++ b/app/src/main/res/layout/activity_create.xml @@ -5,6 +5,7 @@ style="@style/AppTheme" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/colorPrimary" tools:context=".CreateActivity"> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="1.0" /> - + app:layout_constraintTop_toBottomOf="@+id/toolbar"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_text_input.xml b/app/src/main/res/layout/fragment_text_input.xml index af3e08d..3c68aa6 100644 --- a/app/src/main/res/layout/fragment_text_input.xml +++ b/app/src/main/res/layout/fragment_text_input.xml @@ -17,7 +17,7 @@ android:textSize="20sp" /> + + + + + + +
+

OffPass

+

Created on $DATE

+ +

Password hint

+

$HINT

+ + +
+
+
+ + \ No newline at end of file