2FA codes can now be scanned.

This commit is contained in:
2020-07-07 15:10:36 +02:00
parent 576e16a2fc
commit a6a9d95042
14 changed files with 464 additions and 56 deletions

View File

@@ -42,6 +42,10 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.core:core-ktx:1.3.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" 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 / / QRCode reader /
implementation 'com.journeyapps:zxing-android-embedded:4.1.0' implementation 'com.journeyapps:zxing-android-embedded:4.1.0'

View File

@@ -15,6 +15,7 @@
<activity <activity
android:name=".CreateActivity" android:name=".CreateActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:windowSoftInputMode="adjustPan"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@@ -1,25 +1,42 @@
package com.github.mondei1.offpass 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.os.Bundle
import android.text.Editable
import android.util.Log import android.util.Log
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity 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.android.synthetic.main.activity_create.*
import kotlinx.coroutines.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class CreateActivity : AppCompatActivity() { class CreateActivity : AppCompatActivity() {
private var fragment_title: TextInput? = null private var fragment_title: TextInput? = null
private var fragment_username: TextInput? = null private var fragment_username: TextInput? = null
private var schema: QRSchema? = null private var schema: QRSchema? = null
private var fa_coroutine: Job? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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 = 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!!.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) //this.schema!!.parse(this)
@@ -34,6 +51,80 @@ class CreateActivity : AppCompatActivity() {
Log.i("CREATE", "Back got clicked!") Log.i("CREATE", "Back got clicked!")
finish() 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"))
} }
} }

View File

@@ -7,6 +7,9 @@ import android.util.Log
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.security.SecureRandom import java.security.SecureRandom
import java.security.spec.KeySpec import java.security.spec.KeySpec
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.SecretKey import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory import javax.crypto.SecretKeyFactory
@@ -27,13 +30,25 @@ class CryptoOperations {
private val possibleChars: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" private val possibleChars: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
private lateinit var chars: CharArray 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 * @return a secure authentication token to be stored for later authentication
*/ */
fun hash(password: CharArray, salt: ByteArray): String { 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 factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val hash = factory.generateSecret(spec).encoded val hash = factory.generateSecret(spec).encoded
@@ -41,6 +56,12 @@ class CryptoOperations {
return Base64.encodeToString(hash, Base64.DEFAULT) 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 { fun encrypt(key: String, plain: String): EncryptionResult {
val salt = nextString(10).toByteArray() val salt = nextString(10).toByteArray()
val iv = nextString(16).toByteArray() val iv = nextString(16).toByteArray()
@@ -56,10 +77,10 @@ class CryptoOperations {
} }
val key: SecretKey = SecretKeySpec(aes_key, "AES") 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 // 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)}") Log.i("Crypto", "Encrypt using ${String(aes_key)} '$plain' ${String(iv)}")
val ciphered = cipher.doFinal(plain.toByteArray(Charsets.UTF_8)) 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) 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 { fun decrypt(encrypted: String, key: String, iv: ByteArray, salt: ByteArray): String {
val key_bytes: ByteArray = hash(key.toCharArray(), salt).toByteArray(Charsets.UTF_8) val key_bytes: ByteArray = hash(key.toCharArray(), salt).toByteArray(Charsets.UTF_8)
val aes_key: ByteArray = ByteArray(32) 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)}") 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 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 // 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) val ciphered = cipher.doFinal(encrypted_raw)
// Concat everything into one byte array // Concat everything into one byte array
@@ -98,6 +127,11 @@ class CryptoOperations {
return String(byteBuffer.array()) return String(byteBuffer.array())
} }
/**
* Generates a random IV used for encryption.
*
* @return Random IV of a length of 12 bytes
*/
fun nextIV(): ByteArray { fun nextIV(): ByteArray {
val iv: ByteArray = ByteArray(12) val iv: ByteArray = ByteArray(12)
val random: SecureRandom = SecureRandom() val random: SecureRandom = SecureRandom()
@@ -106,6 +140,9 @@ class CryptoOperations {
return iv return iv
} }
/**
* Produces secure random string. <b>This function can be slow, use only if necessary!</b>
*/
fun nextString(length: Int): String { fun nextString(length: Int): String {
chars = CharArray(length) chars = CharArray(length)
for (idx in chars.indices) chars[idx] = possibleChars[random.nextInt(possibleChars.length)] for (idx in chars.indices) chars[idx] = possibleChars[random.nextInt(possibleChars.length)]
@@ -114,17 +151,4 @@ class CryptoOperations {
return String(chars) 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)
}
} }

View File

@@ -46,7 +46,6 @@ class MainActivity : AppCompatActivity() {
.setBeepEnabled(false) .setBeepEnabled(false)
.setBarcodeImageEnabled(false) .setBarcodeImageEnabled(false)
.initiateScan() .initiateScan()
Log.i("Main", "Clicked on text")
} }
add_button.setOnClickListener { add_button.setOnClickListener {

View File

@@ -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 { private suspend fun resolve(key: String, context: Context): String {
val to_resolve = key.replace("$", "") 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. * 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. * @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 { fun parse(context: Context): Boolean {
if (decrypted_raw == "") { if (decrypted_raw == "") {
@@ -61,7 +64,7 @@ class QRSchema {
} }
// First will be to split the string into it's parts // 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) { for (i in 0 until fields.size) {
// First four items have to exist and are title, username, password, URL in this order. // 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 // Look if website url is compressed
if (this.website_url.startsWith("$")) { if (this.website_url.startsWith("$")) {
var that_class = this val that_class = this
runBlocking { runBlocking {
that_class.website_url = resolve(that_class.website_url, context) that_class.website_url = resolve(that_class.website_url, context)
} }
@@ -111,7 +114,7 @@ class QRSchema {
// We got a security question/awnser // We got a security question/awnser
if (key == "") { if (key == "") {
var qa = value.split("%") val qa = value.split("%")
question_awnser.put(qa[0], qa[1]); question_awnser.put(qa[0], qa[1]);
} else { } else {
custom.put(key, value) custom.put(key, value)
@@ -131,7 +134,7 @@ class QRSchema {
/** /**
* This function will take an raw string input and will decrypt it. * 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 { fun decrypt(raw: String, passphrase: String): String {
val parts = raw.split(":") val parts = raw.split(":")

View File

@@ -44,6 +44,7 @@ class TextInput : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
head.text = arg_head head.text = arg_head
title_.hint = arg_title
} }
companion object { companion object {

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,6 +5,7 @@
style="@style/AppTheme" style="@style/AppTheme"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context=".CreateActivity"> tools:context=".CreateActivity">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
@@ -48,50 +49,255 @@
<ImageButton <ImageButton
android:id="@+id/settings" android:id="@+id/settings"
style="@style/Widget.AppCompat.Button.Borderless" style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="56dp" android:layout_width="32dp"
android:layout_height="50dp" android:layout_height="31dp"
android:layout_marginEnd="60dp" android:layout_marginTop="8dp"
android:layout_marginEnd="72dp"
android:layout_marginBottom="6dp"
android:scaleX="1" android:scaleX="1"
android:scaleY="1" android:scaleY="1"
android:src="@drawable/baseline_settings_white_36" android:src="@drawable/baseline_settings_white_36"
android:text="" android:text=""
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ImageButton <ImageButton
android:id="@+id/settings2" android:id="@+id/settings2"
style="@style/Widget.AppCompat.Button.Borderless" style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="54dp" android:layout_width="38dp"
android:layout_height="37dp" android:layout_height="38dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="116dp" android:layout_marginEnd="116dp"
android:layout_marginBottom="3dp"
android:src="@drawable/ic_compress" android:src="@drawable/ic_compress"
android:text="" android:text=""
android:textColor="#000000" android:textColor="#000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>
<FrameLayout <ScrollView
android:id="@+id/title_box" android:layout_width="416dp"
android:name="com.github.mondei1.offpass.TextInput" android:layout_height="566dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" /> app:layout_constraintTop_toBottomOf="@+id/toolbar">
<FrameLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/username" android:layout_width="match_parent"
android:name="com.github.mondei1.offpass.TextInput" android:layout_height="wrap_content">
android:layout_width="wrap_content"
android:layout_height="wrap_content" <TextView
android:layout_marginStart="16dp" android:id="@+id/url_label"
android:layout_marginTop="10dp" android:layout_width="wrap_content"
app:layout_constraintStart_toStartOf="parent" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/title_box" /> android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:text="Login URL"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/email_input" />
<EditText
android:id="@+id/email_input"
style="@style/Widget.AppCompat.AutoCompleteTextView"
android:layout_width="354dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:background="@color/colorPrimary"
android:ems="10"
android:fontFamily="sans-serif-black"
android:hint="ricklactica42@example.com"
android:inputType="none|textEmailAddress"
android:singleLine="true"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/email_label" />
<TextView
android:id="@+id/username_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:text="Username"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_input" />
<EditText
android:id="@+id/password_input"
android:layout_width="288dp"
android:layout_height="68dp"
android:layout_marginStart="16dp"
android:background="@color/colorPrimary"
android:ems="10"
android:fontFamily="sans-serif-black"
android:hint="You shouldn't see that"
android:inputType="textPassword"
android:singleLine="true"
android:textSize="28sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password_label" />
<EditText
android:id="@+id/username_input"
style="@style/Widget.AppCompat.AutoCompleteTextView"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:background="@color/colorPrimary"
android:ems="10"
android:fontFamily="sans-serif-black"
android:hint="Ricklactica42"
android:inputType="textPersonName"
android:singleLine="true"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username_label" />
<TextView
android:id="@+id/password_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:text="Password"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username_input" />
<EditText
android:id="@+id/url_input"
style="@style/Widget.AppCompat.AutoCompleteTextView"
android:layout_width="354dp"
android:layout_height="48dp"
android:layout_marginStart="15dp"
android:background="@color/colorPrimary"
android:ems="10"
android:fontFamily="sans-serif-black"
android:hint="https://example.com/login"
android:inputType="none|textEmailAddress"
android:singleLine="true"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/url_label" />
<EditText
android:id="@+id/title_input"
style="@style/Widget.AppCompat.AutoCompleteTextView"
android:layout_width="283dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:background="@color/colorPrimary"
android:ems="10"
android:fontFamily="sans-serif-black"
android:hint="Google Account"
android:inputType="textPersonName"
android:singleLine="true"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_label" />
<TextView
android:id="@+id/email_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:text="E-Mail"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password_input" />
<TextView
android:id="@+id/title_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:text="Title"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/password_hide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="@+id/password_input"
app:layout_constraintStart_toEndOf="@+id/password_input"
app:layout_constraintTop_toTopOf="@+id/password_input"
app:layout_constraintVertical_bias="0.48000002"
app:srcCompat="@drawable/baseline_visibility_black_36" />
<ImageButton
android:id="@+id/password_random"
android:layout_width="31dp"
android:layout_height="38dp"
android:layout_marginStart="8dp"
android:background="@color/colorPrimary"
android:tint="#000000"
app:layout_constraintBottom_toBottomOf="@+id/password_input"
app:layout_constraintStart_toEndOf="@+id/password_hide"
app:layout_constraintTop_toTopOf="@+id/password_input"
app:srcCompat="@drawable/baseline_sd_storage_24" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="110dp"
android:background="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="@+id/fa_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Two factor authentication"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/fa_input"
style="@style/Widget.AppCompat.AutoCompleteTextView"
android:layout_width="354dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:background="@color/colorPrimary"
android:editable="false"
android:ems="10"
android:fontFamily="sans-serif-black"
android:hint="TAP TO SCAN"
android:inputType="none|textEmailAddress"
android:singleLine="true"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fa_label" />
<ProgressBar
android:id="@+id/fa_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="379dp"
android:layout_height="15dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:max="30"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fa_input" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,7 +17,7 @@
android:textSize="20sp" /> android:textSize="20sp" />
<EditText <EditText
android:id="@+id/title_box" android:id="@+id/title_"
style="@style/Widget.AppCompat.EditText" style="@style/Widget.AppCompat.EditText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<!-- Head is not important since
we will only print the body-->
</head>
<body>
<style>
@font-face {
font-family: Roboto;
src: url(font/robotolight.ttf);
font-weight: lighter;
}
@font-face {
font-family: Roboto;
src: url(font/robotoregular.ttf);
font-weight: normal;
}
@font-face {
font-family: Roboto;
src: url(font/robotobold.ttf);
font-weight: bold;
}
* {
text-align: center;
font-family: Roboto;
}
h1 {
margin-top: 8rem;
font-size: 36pt;
}
p {
font-size: 24pt;
}
#hint_title {
margin-top: 10rem;
font-weight: bold;
}
img {
margin-top: 5rem;
height: 400px;
}
/* Background stripes */
#red {
position: absolute;
bottom: -350px;
left: 0;
z-index: -1;
width: 340px;
height: 815px;
background-color: #FF2A2A;
transform: rotate(130deg);
}
#black {
position: absolute;
bottom: -320px;
right: 0;
z-index: -1;
width: 380px;
height: 815px;
background-color: #1C1C1C;
transform: rotate(-130deg);
}
</style>
<div id="content">
<h1>OffPass</h1>
<p>Created on $DATE</p>
<p id="hint_title">Password hint</p>
<p>$HINT</p>
<img src="drawable/dummy_qr_code.png">
<div id="red"></div>
<div id="black"></div>
</div>
</body>
</html>