2FA codes can now be scanned.
This commit is contained in:
@@ -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'
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)]
|
||||||
@@ -115,16 +152,3 @@ class CryptoOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
.setBeepEnabled(false)
|
||||||
.setBarcodeImageEnabled(false)
|
.setBarcodeImageEnabled(false)
|
||||||
.initiateScan()
|
.initiateScan()
|
||||||
Log.i("Main", "Clicked on text")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add_button.setOnClickListener {
|
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 {
|
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(":")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
BIN
app/src/main/res/drawable/dummy_qr_code.png
Normal file
BIN
app/src/main/res/drawable/dummy_qr_code.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
BIN
app/src/main/res/font/robotobold.ttf
Normal file
BIN
app/src/main/res/font/robotobold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/robotoregular.ttf
Normal file
BIN
app/src/main/res/font/robotoregular.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/robotothin.ttf
Normal file
BIN
app/src/main/res/font/robotothin.ttf
Normal file
Binary file not shown.
@@ -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">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/url_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="20dp"
|
||||||
|
android:text="Login URL"
|
||||||
|
android:textSize="18sp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/title_box" />
|
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>
|
||||||
@@ -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"
|
||||||
|
|||||||
79
app/src/main/res/print.html
Normal file
79
app/src/main/res/print.html
Normal 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>
|
||||||
Reference in New Issue
Block a user