Compare commits
11 Commits
099d4df53d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
f8e143db12
|
|||
|
83328f42ec
|
|||
|
3ffd0e063f
|
|||
|
2efdce87a8
|
|||
|
1bb1b55452
|
|||
|
8865e9a394
|
|||
|
a6a9d95042
|
|||
|
576e16a2fc
|
|||
|
b1b4ad3030
|
|||
|
a8e6c9e99b
|
|||
|
1d020c5110
|
@@ -16,6 +16,7 @@ android {
|
|||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -34,12 +35,27 @@ repositories {
|
|||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
def room_version = "2.2.5"
|
||||||
|
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.3.0'
|
implementation 'androidx.core:core-ktx:1.3.0'
|
||||||
|
|
||||||
|
/* Password strengh library */
|
||||||
|
compile 'com.nulab-inc:zxcvbn: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'
|
implementation 'com.journeyapps:zxing-android-embedded:4.1.0'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
|
|||||||
@@ -6,12 +6,20 @@
|
|||||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<activity android:name=".GeneratorActivity"></activity>
|
||||||
|
<activity android:name=".ViewActivity" />
|
||||||
|
<activity android:name=".PassphraseActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".CreateActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustPan" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:screenOrientation="portrait">
|
android:screenOrientation="portrait">
|
||||||
@@ -21,6 +29,7 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="preloaded_fonts"
|
android:name="preloaded_fonts"
|
||||||
android:resource="@array/preloaded_fonts" />
|
android:resource="@array/preloaded_fonts" />
|
||||||
|
|||||||
BIN
app/src/main/assets/dummy_qr_code.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
app/src/main/assets/print_background.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
app/src/main/assets/robotobold.ttf
Normal file
BIN
app/src/main/assets/robotoregular.ttf
Normal file
BIN
app/src/main/assets/robotothin.ttf
Normal file
458
app/src/main/java/com/github/mondei1/offpass/CreateActivity.kt
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
package com.github.mondei1.offpass
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.*
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.print.PrintAttributes
|
||||||
|
import android.print.PrintJob
|
||||||
|
import android.print.PrintManager
|
||||||
|
import android.text.method.HideReturnsTransformationMethod
|
||||||
|
import android.text.method.PasswordTransformationMethod
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.ContextThemeWrapper
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
import com.google.zxing.integration.android.IntentResult
|
||||||
|
import com.journeyapps.barcodescanner.BarcodeEncoder
|
||||||
|
import com.nulabinc.zxcvbn.Zxcvbn
|
||||||
|
import dev.turingcomplete.kotlinonetimepassword.GoogleAuthenticator
|
||||||
|
import kotlinx.android.synthetic.main.activity_create.*
|
||||||
|
import kotlinx.android.synthetic.main.activity_generator.*
|
||||||
|
import kotlinx.android.synthetic.main.activity_generator.view.*
|
||||||
|
import kotlinx.android.synthetic.main.dialogpassphrase.view.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.lang.IllegalArgumentException
|
||||||
|
import java.lang.StringBuilder
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
class CreateActivity : AppCompatActivity() {
|
||||||
|
private var schema: QRSchema? = null
|
||||||
|
private var qrCodeBitmap: Bitmap? = null
|
||||||
|
|
||||||
|
private var fa_coroutine: Job? = null
|
||||||
|
private var mWebView: WebView? = null
|
||||||
|
private var printJob: PrintJob? = null
|
||||||
|
|
||||||
|
private var fa_uri: Uri? = null
|
||||||
|
|
||||||
|
private var known_passwords = ArrayList<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets triggered when the user clicks on printing button (top left) and renders the paper to
|
||||||
|
* print.
|
||||||
|
*/
|
||||||
|
fun doPrint(passphrase: String, hint: String) {
|
||||||
|
// Set up printing
|
||||||
|
this.schema = QRSchema(this)
|
||||||
|
this.schema!!.title = title_input.text.toString()
|
||||||
|
this.schema!!.username = username_input.text.toString()
|
||||||
|
this.schema!!.password = password_input.text.toString()
|
||||||
|
this.schema!!.email = email_input.text.toString()
|
||||||
|
this.schema!!.website_url = url_input.text.toString()
|
||||||
|
|
||||||
|
if (fa_uri != null) {
|
||||||
|
this.schema!!.custom.put("2fa", fa_uri.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schema!!.build(arrayOf(), passphrase)
|
||||||
|
|
||||||
|
val barcodeEncoder: BarcodeEncoder = BarcodeEncoder()
|
||||||
|
this.qrCodeBitmap = barcodeEncoder.encodeBitmap(this.schema!!.raw, BarcodeFormat.QR_CODE, 400, 400)
|
||||||
|
|
||||||
|
|
||||||
|
val webView = WebView(this)
|
||||||
|
webView.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView, requesst: WebResourceRequest) = false
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
Log.i("Create Activity", "Template page finished loading")
|
||||||
|
createWebPrintJob(webView)
|
||||||
|
mWebView = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var htmlDocument = String(this.resources.openRawResource(
|
||||||
|
this.resources.getIdentifier("print", "raw", this.packageName)
|
||||||
|
).readBytes()).replace("\n", "")
|
||||||
|
|
||||||
|
// Prepare html document
|
||||||
|
var byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
|
this.qrCodeBitmap?.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||||
|
htmlDocument = htmlDocument.replace("\$DATE", Date().time.toString())
|
||||||
|
.replace("\$HINT", hint)
|
||||||
|
.replace("\$QRCODE", Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP))
|
||||||
|
|
||||||
|
Log.i("Create Activity", htmlDocument)
|
||||||
|
webView.loadDataWithBaseURL("file:///android_asset/", htmlDocument, "text/HTML", "UTF-8", null)
|
||||||
|
|
||||||
|
mWebView = webView
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked after doPrint() and takes the rendered web view and creates a new print job.
|
||||||
|
*/
|
||||||
|
fun createWebPrintJob(webView: WebView) {
|
||||||
|
(this.getSystemService(Context.PRINT_SERVICE) as? PrintManager)?.let { printManager ->
|
||||||
|
val jobName = "Offpass Document"
|
||||||
|
val printAdapter = webView.createPrintDocumentAdapter(jobName)
|
||||||
|
|
||||||
|
printManager.print(
|
||||||
|
jobName,
|
||||||
|
printAdapter,
|
||||||
|
PrintAttributes.Builder().build()
|
||||||
|
).also { printJob ->
|
||||||
|
this.printJob = printJob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when user gave empty passphrase.
|
||||||
|
*/
|
||||||
|
fun showError(title: String, body: String) {
|
||||||
|
val builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.ErrorDialog))
|
||||||
|
|
||||||
|
with(builder) {
|
||||||
|
setTitle(title)
|
||||||
|
setMessage(body)
|
||||||
|
setPositiveButton("Got it", DialogInterface.OnClickListener { dialogInterface, i ->
|
||||||
|
dialogInterface.dismiss()
|
||||||
|
})
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun passwordKnown(password: String): Boolean {
|
||||||
|
// TODO: Doesn't work on all devices somehow :shrug:
|
||||||
|
return this.known_passwords.contains(password.toLowerCase(Locale.ROOT))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Prevent other apps from making screenshots or to record Offpass.
|
||||||
|
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
this.schema!!.build(arrayOf("website_url", "2fa", "What's your favorite series"), "123")
|
||||||
|
this.schema!!.decrypt(this.schema!!.raw, "123")*/
|
||||||
|
|
||||||
|
setSupportActionBar(findViewById(R.id.toolbar))
|
||||||
|
|
||||||
|
// Split word list in the background (this process can take a few seconds to complete)
|
||||||
|
GlobalScope.async {
|
||||||
|
val known_passwords_ = String(resources.openRawResource(
|
||||||
|
resources.getIdentifier("passwordlist", "raw", packageName)
|
||||||
|
).readBytes())
|
||||||
|
|
||||||
|
known_passwords = known_passwords_.split("\n") as ArrayList<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_create)
|
||||||
|
back.setOnClickListener {
|
||||||
|
Log.i("CREATE", "Back got clicked!")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
print_button.setOnClickListener {
|
||||||
|
fun continueAfterCheck() {
|
||||||
|
// Ask user for passhprase and hint
|
||||||
|
val builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.PassphraseDialog))
|
||||||
|
|
||||||
|
with(builder) {
|
||||||
|
val editTextLayout = layoutInflater.inflate(R.layout.dialogpassphrase, null)
|
||||||
|
setView(editTextLayout)
|
||||||
|
setTitle("Set Passphrase")
|
||||||
|
setMessage("Final step is it to set your passphrase. Minimum are 8 characters.")
|
||||||
|
setPositiveButton("Print", DialogInterface.OnClickListener { dialogInterface, i ->
|
||||||
|
val passphrase = editTextLayout.passphrase_input.text.toString();
|
||||||
|
if (passphrase == "") {
|
||||||
|
// Make a new alert, telling the user that passphrase must not be null.
|
||||||
|
dialogInterface.cancel()
|
||||||
|
showError("Empty passphrase", "Passphrase must not be null.")
|
||||||
|
return@OnClickListener
|
||||||
|
}
|
||||||
|
if (passphrase.length < 8) {
|
||||||
|
// Make a new alert, telling the user that his passphrase doesn't meet the minimum.
|
||||||
|
dialogInterface.cancel()
|
||||||
|
showError("Weak passphrase", "Passphrase has to be at least 8 characters long.")
|
||||||
|
return@OnClickListener
|
||||||
|
}
|
||||||
|
if (passphrase != editTextLayout.passphrase2_input.text.toString()) {
|
||||||
|
// Make a new alert, telling the user that his passphrase doesn't match the repeat field.
|
||||||
|
dialogInterface.cancel()
|
||||||
|
showError("Passphrase mismatch", "Both passphrases do not match.")
|
||||||
|
return@OnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if password is found in our offline password list
|
||||||
|
if(passwordKnown(passphrase)) {
|
||||||
|
// Make a new alert, telling the user that his passphrase was found in our local wordlist.
|
||||||
|
dialogInterface.cancel()
|
||||||
|
showError("Passphrase is compromised!", "Passphrase got found in a wordlist of bad passwords. " +
|
||||||
|
"If you already used it somewhere, change it immediately!")
|
||||||
|
return@OnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
doPrint(editTextLayout.passphrase_input.text.toString(),
|
||||||
|
editTextLayout.hint_input.text.toString())
|
||||||
|
})
|
||||||
|
setNegativeButton("Go back", DialogInterface.OnClickListener { dialogInterface, i ->
|
||||||
|
dialogInterface.dismiss()
|
||||||
|
})
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if entered password is compromised
|
||||||
|
if (passwordKnown(password_input.text.toString())) {
|
||||||
|
val builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.ErrorDialog))
|
||||||
|
|
||||||
|
with(builder) {
|
||||||
|
setTitle("Password compromised!")
|
||||||
|
setMessage("Your password is weak and should be changed as it got found in a list of weak passwords.")
|
||||||
|
setPositiveButton("I'll change it", DialogInterface.OnClickListener { dialogInterface, i ->
|
||||||
|
})
|
||||||
|
setNegativeButton("I don't care", DialogInterface.OnClickListener { dialogInterface, i ->
|
||||||
|
continueAfterCheck()
|
||||||
|
})
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continueAfterCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
password_hide.setOnClickListener {
|
||||||
|
if (password_input.transformationMethod == HideReturnsTransformationMethod.getInstance()) {
|
||||||
|
password_input.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||||
|
password_hide.setImageResource(R.drawable.ic_visibility_on)
|
||||||
|
} else {
|
||||||
|
password_input.transformationMethod = HideReturnsTransformationMethod.getInstance()
|
||||||
|
password_hide.setImageResource(R.drawable.ic_visibility_off)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
password_random.setOnClickListener {
|
||||||
|
val builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.PassphraseGenerator))
|
||||||
|
|
||||||
|
with (builder) {
|
||||||
|
val layout = layoutInflater.inflate(R.layout.activity_generator, null)
|
||||||
|
val zxcvbn = Zxcvbn()
|
||||||
|
var length: Int = 8
|
||||||
|
setTitle("Generate password")
|
||||||
|
setView(layout)
|
||||||
|
|
||||||
|
fun newPassword() {
|
||||||
|
var combined = ""
|
||||||
|
val upper_case = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
val lower_case = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
val speicals = "!\";#$%&'()*+,-./:;<=>?@[/]^_`"
|
||||||
|
val digits = "1234567890"
|
||||||
|
val emojis = arrayOf(0x1F600, 0x1F603, 0x1F601, 0x1F911, 0x1F910, 0x1F637, 0x1F47D, 0x1F480, 0x1F916)
|
||||||
|
val rand: Random = Random()
|
||||||
|
|
||||||
|
if (layout.AZ.isChecked) combined += upper_case
|
||||||
|
if (layout.az.isChecked) combined += lower_case
|
||||||
|
if (layout.zero_nine.isChecked) combined += digits
|
||||||
|
if (layout.emojis.isChecked) {
|
||||||
|
emojis.forEach {
|
||||||
|
combined += String(Character.toChars(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (layout.special.isChecked) combined += speicals
|
||||||
|
|
||||||
|
val sb: StringBuilder = StringBuilder(length)
|
||||||
|
for (x in 1..length) {
|
||||||
|
sb.append(combined[rand.nextInt(combined.length)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapt text size
|
||||||
|
if (sb.length > 10) {
|
||||||
|
layout.random_string.textSize = 24.0f
|
||||||
|
} else if (sb.length > 16) {
|
||||||
|
layout.random_string.textSize = 18.0f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure strengh
|
||||||
|
layout.progressBar.progress = zxcvbn.measure(sb.toString())
|
||||||
|
.score
|
||||||
|
|
||||||
|
if (layout.progressBar.progress == 1) {
|
||||||
|
layout.progressBar.progressTintList = ColorStateList.valueOf(Color.RED)
|
||||||
|
} else if (layout.progressBar.progress == 2) {
|
||||||
|
layout.progressBar.progressTintList = ColorStateList.valueOf(Color.YELLOW)
|
||||||
|
} else if (layout.progressBar.progress == 3) {
|
||||||
|
layout.progressBar.progressTintList = ColorStateList.valueOf(Color.GREEN)
|
||||||
|
} else {
|
||||||
|
layout.progressBar.progressTintList = ColorStateList.valueOf(Color.GREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.random_string.setText(sb.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial set
|
||||||
|
layout.length_label.setText(length.toString())
|
||||||
|
newPassword()
|
||||||
|
|
||||||
|
// Add length
|
||||||
|
layout.add_button.setOnClickListener {
|
||||||
|
length++
|
||||||
|
layout.length_label.setText(length.toString())
|
||||||
|
newPassword()
|
||||||
|
}
|
||||||
|
layout.add_button.setOnLongClickListener {
|
||||||
|
length += 10
|
||||||
|
layout.length_label.setText(length.toString())
|
||||||
|
newPassword()
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract length
|
||||||
|
layout.subtract_button.setOnClickListener {
|
||||||
|
length--
|
||||||
|
layout.length_label.setText(length.toString())
|
||||||
|
newPassword()
|
||||||
|
}
|
||||||
|
layout.subtract_button.setOnLongClickListener {
|
||||||
|
length -= 10
|
||||||
|
layout.length_label.setText(length.toString())
|
||||||
|
newPassword()
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh
|
||||||
|
layout.refresh_button.setOnClickListener {
|
||||||
|
newPassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy password
|
||||||
|
layout.copy_button.setOnClickListener {
|
||||||
|
// Copy code if already scanend
|
||||||
|
val clip: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
clip.setPrimaryClip(ClipData.newPlainText("Random password", layout.random_string.text.toString()))
|
||||||
|
Toast.makeText(applicationContext, "Copied!", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
setPositiveButton("Use this",
|
||||||
|
DialogInterface.OnClickListener { dialog, id ->
|
||||||
|
password_input.setText(layout.random_string.text)
|
||||||
|
})
|
||||||
|
setNegativeButton("Go back",
|
||||||
|
DialogInterface.OnClickListener { dialog, id ->
|
||||||
|
dialog.dismiss()
|
||||||
|
})
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
fa_uri = Uri.parse(result.contents)
|
||||||
|
|
||||||
|
if (fa_uri?.getQueryParameter("secret").toString() == "" || fa_uri?.getQueryParameter("secret") == null) {
|
||||||
|
showError("Corrupt 2FA", "The scanned 2FA secret doesn't contain a secret!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val fa_generator = GoogleAuthenticator(base32secret = fa_uri?.getQueryParameter("secret").toString())
|
||||||
|
this.schema!!.custom.put("2fa", 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) {
|
||||||
|
try {
|
||||||
|
fa_input.setText(fa_generator.generate())
|
||||||
|
} catch (error: IllegalArgumentException) {
|
||||||
|
showError("Corrupt 2FA", "The scanned 2FA secret is corrupt and therefore no codes can be generated.")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
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")})"
|
||||||
|
} else {
|
||||||
|
showError("No 2FA", "The scanned QR-Code has the wrong format.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
fa_coroutine?.cancel(CancellationException("Activity is stopping"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
154
app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package com.github.mondei1.offpass
|
||||||
|
|
||||||
|
import android.os.HandlerThread
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Base64
|
||||||
|
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
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class EncryptionResult(
|
||||||
|
val result: String = "",
|
||||||
|
val salt: ByteArray = ByteArray(1),
|
||||||
|
val iv: ByteArray = ByteArray(1)
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptoOperations {
|
||||||
|
private val random: SecureRandom = SecureRandom()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, 8000, 256)
|
||||||
|
val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||||
|
val hash = factory.generateSecret(spec).encoded
|
||||||
|
|
||||||
|
Log.i("Crypto", "Hashed ${password.toString()} to ${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 {
|
||||||
|
val salt = nextString(10).toByteArray()
|
||||||
|
val iv = nextString(16).toByteArray()
|
||||||
|
val key_bytes: ByteArray = hash(key.toCharArray(), salt).toByteArray(Charsets.UTF_8)
|
||||||
|
val aes_key: ByteArray = ByteArray(32)
|
||||||
|
|
||||||
|
// Now make the key bytes fit into 256 bits
|
||||||
|
if (key_bytes.size < aes_key.size) {
|
||||||
|
throw Error("Tried to cut down hash bytes to 32 but there are less then that.")
|
||||||
|
}
|
||||||
|
for (i in aes_key.indices) {
|
||||||
|
aes_key.set(i, key_bytes[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
val key: SecretKey = SecretKeySpec(aes_key, "AES")
|
||||||
|
val cipher: Cipher = Cipher.getInstance("AES/CBC/ISO10126Padding")
|
||||||
|
|
||||||
|
// Performing actual crypto operation
|
||||||
|
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))
|
||||||
|
|
||||||
|
Log.d("Crypto", "Encrypted $plain => ${Base64.encodeToString(ciphered, Base64.NO_WRAP)}")
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Now make the key bytes fit into 256
|
||||||
|
if (key_bytes.size < aes_key.size) {
|
||||||
|
throw Error("Tried to cut down hash bytes to 32 but there are less then that.")
|
||||||
|
}
|
||||||
|
for (i in 0..aes_key.size-1) {
|
||||||
|
aes_key.set(i, key_bytes[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
val encrypted_raw = Base64.decode(encrypted, Base64.NO_WRAP)
|
||||||
|
|
||||||
|
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/CBC/ISO10126Padding")
|
||||||
|
|
||||||
|
// Performing actual crypto operation
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, IvParameterSpec(iv))
|
||||||
|
val ciphered = cipher.doFinal(encrypted_raw) // TODO: Catch error if decryption goes wrong
|
||||||
|
|
||||||
|
// Concat everything into one byte array
|
||||||
|
val byteBuffer: ByteBuffer = ByteBuffer.allocate(ciphered.size)
|
||||||
|
byteBuffer.put(ciphered)
|
||||||
|
|
||||||
|
Log.d("Crypto", "Decrypted $encrypted => ${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 {
|
||||||
|
val iv: ByteArray = ByteArray(12)
|
||||||
|
val random: SecureRandom = SecureRandom()
|
||||||
|
random.nextBytes(iv)
|
||||||
|
|
||||||
|
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)]
|
||||||
|
Log.i("Random", "Produced random string: ${String(chars)}")
|
||||||
|
|
||||||
|
return String(chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.github.mondei1.offpass
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.android.synthetic.main.activity_generator.*
|
||||||
|
|
||||||
|
class GeneratorActivity : AppCompatActivity() {
|
||||||
|
var length: Int = 12
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_generator)
|
||||||
|
|
||||||
|
length_label.setText(length.toString())
|
||||||
|
Log.i("Generator", "Length: $length")
|
||||||
|
add_button.setOnClickListener {
|
||||||
|
Log.i("Generator", "Length: $length")
|
||||||
|
length++
|
||||||
|
length_label.setText(length.toString())
|
||||||
|
}
|
||||||
|
subtract_button.setOnClickListener {
|
||||||
|
Log.i("Generator", "Length: $length")
|
||||||
|
length--
|
||||||
|
length_label.setText(length.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,13 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.github.mondei1.offpass.entities.Compression
|
||||||
|
import com.github.mondei1.offpass.entities.CompressionHelper
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import com.google.zxing.integration.android.IntentResult
|
import com.google.zxing.integration.android.IntentResult
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -17,13 +21,22 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
var dbHandler = CompressionHelper(this, null)
|
||||||
|
dbHandler.writableDatabase.execSQL("DELETE FROM `${CompressionHelper.TABLE_NAME}`;")
|
||||||
|
|
||||||
|
dbHandler.writableDatabase.close()
|
||||||
|
|
||||||
main_screen.setOnClickListener {
|
main_screen.setOnClickListener {
|
||||||
IntentIntegrator(this)
|
IntentIntegrator(this)
|
||||||
.setPrompt("Scan OffPass created QR-Code")
|
.setPrompt("Scan OffPass created QR-Code")
|
||||||
.setBeepEnabled(false)
|
.setBeepEnabled(false)
|
||||||
.setBarcodeImageEnabled(false)
|
.setBarcodeImageEnabled(false)
|
||||||
.initiateScan()
|
.initiateScan()
|
||||||
Log.i("Main", "Clicked on text")
|
}
|
||||||
|
|
||||||
|
add_button.setOnClickListener {
|
||||||
|
val intent: Intent = Intent(this, CreateActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,15 +44,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
var result: IntentResult =
|
var result: IntentResult =
|
||||||
IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||||
if (result != null) {
|
|
||||||
if (result.contents == null) {
|
if (result.contents == null) {
|
||||||
Toast.makeText(this, "Cancelled", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "Cancelled", Toast.LENGTH_LONG).show()
|
||||||
} else {
|
} else {
|
||||||
Log.i("SCANNER", "Scanned: " + result.contents)
|
// TODO: Check if scanned QR-Code is an OffPass one.
|
||||||
Toast.makeText(this, "Scanned: " + result.contents, Toast.LENGTH_LONG).show()
|
val intent: Intent = Intent(this, PassphraseActivity::class.java)
|
||||||
}
|
intent.putExtra("raw", result.contents)
|
||||||
} else {
|
startActivity(intent)
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
//Toast.makeText(this, "Scanned: " + result.contents, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.github.mondei1.offpass
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.view.View
|
||||||
|
import kotlinx.android.synthetic.main.activity_passphrase.*
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import javax.crypto.BadPaddingException
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
|
class PassphraseActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_passphrase)
|
||||||
|
|
||||||
|
val raw: String = intent.getStringExtra("raw")!!
|
||||||
|
val qrSchema: QRSchema = QRSchema(this)
|
||||||
|
|
||||||
|
back.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt_button.setOnClickListener {
|
||||||
|
decrypt_button.text = "Decrypting ..."
|
||||||
|
var _continue = true;
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
try {
|
||||||
|
qrSchema.decrypt(raw, passphrase_input.text.toString())
|
||||||
|
} catch (err: BadPaddingException) {
|
||||||
|
failed.visibility = View.VISIBLE
|
||||||
|
decrypt_button.text = "Decrypt"
|
||||||
|
_continue = false
|
||||||
|
return@runBlocking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!_continue) return@setOnClickListener
|
||||||
|
|
||||||
|
val intent: Intent = Intent(this, ViewActivity::class.java)
|
||||||
|
intent.putExtra("decrypted_raw",qrSchema.decrypted_raw)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
234
app/src/main/java/com/github/mondei1/offpass/QRSchema.kt
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
package com.github.mondei1.offpass
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import com.github.mondei1.offpass.entities.Compression
|
||||||
|
import com.github.mondei1.offpass.entities.CompressionHelper
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.lang.Error
|
||||||
|
|
||||||
|
class QRSchema {
|
||||||
|
|
||||||
|
private var db: CompressionHelper? = null
|
||||||
|
|
||||||
|
var raw: String = ""
|
||||||
|
var decrypted_raw: String = ""
|
||||||
|
|
||||||
|
// Parsed content
|
||||||
|
var session_key: String = ""
|
||||||
|
lateinit var title: String
|
||||||
|
lateinit var username: String
|
||||||
|
lateinit var password: String
|
||||||
|
lateinit var email: String
|
||||||
|
lateinit var website_url: String
|
||||||
|
var custom: HashMap<String, String> = HashMap() // All defined custom/optional fields
|
||||||
|
var question_awnser: HashMap<String, String> = HashMap() // Used for security questions
|
||||||
|
|
||||||
|
constructor(context: Context) {
|
||||||
|
this.db = CompressionHelper(context, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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("$", "")
|
||||||
|
Log.i("QR-Code schema", "Try to resolve $to_resolve")
|
||||||
|
val database = db
|
||||||
|
|
||||||
|
return GlobalScope.async<String> {
|
||||||
|
var cursor = database!!.get(session_key, to_resolve)
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.toString()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 == "") {
|
||||||
|
throw Error("Tried to parse QR-Code schema but raw content must be first decrypted! " +
|
||||||
|
"Set raw content first and then use decrypt()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// First will be to split the string into it's parts
|
||||||
|
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.
|
||||||
|
if (i == 0) {
|
||||||
|
// Check if there is a session key
|
||||||
|
if (fields[0].startsWith("%")) {
|
||||||
|
fields[0] = fields[0].replace("%", "")
|
||||||
|
session_key = fields[0].substring(0, 10) // Get first 10 chars, which are the
|
||||||
|
// session key
|
||||||
|
this.title = fields[0].substring(10, fields[0].length)
|
||||||
|
} else {
|
||||||
|
this.title = fields[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.username = fields[1]
|
||||||
|
this.password = fields[2]
|
||||||
|
this.email = fields[3]
|
||||||
|
this.website_url = fields[4]
|
||||||
|
|
||||||
|
// Look if website url is compressed
|
||||||
|
if (this.website_url.startsWith("$")) {
|
||||||
|
val that_class = this
|
||||||
|
runBlocking {
|
||||||
|
that_class.website_url = resolve(that_class.website_url, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here are optional/custom fields
|
||||||
|
if (i > 4) {
|
||||||
|
if (fields[i].startsWith("(") && fields[i] != "") {
|
||||||
|
var closingBracket = fields[i].indexOf(")")
|
||||||
|
var key = fields[i].substring(1, closingBracket)
|
||||||
|
var value = fields[i].substring(closingBracket+1, fields[i].length)
|
||||||
|
|
||||||
|
// We got a security question/awnser
|
||||||
|
if (key == "") {
|
||||||
|
val qa = value.split("%")
|
||||||
|
question_awnser.put(qa[0], qa[1]);
|
||||||
|
} else {
|
||||||
|
// Check if key and or value are compressed
|
||||||
|
if (key.startsWith("$")) {
|
||||||
|
runBlocking {
|
||||||
|
key = resolve(key, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value.startsWith("$")) {
|
||||||
|
runBlocking {
|
||||||
|
value = resolve(value, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
custom.put(key, value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Error("Custom/optional field should start with an open bracket: "
|
||||||
|
+ fields[i].toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i("QR-Code schema", "Found: $session_key, $title, $username, $password, " +
|
||||||
|
"$email, $website_url. ${custom.toString()} and ${question_awnser.toString()}")
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will take an raw string input and will decrypt it.
|
||||||
|
*
|
||||||
|
* @return It returns the decrypted raw value.
|
||||||
|
*/
|
||||||
|
fun decrypt(raw: String, passphrase: String): String {
|
||||||
|
val parts = raw.split(":")
|
||||||
|
val iv = parts[1]
|
||||||
|
val salt = parts[2]
|
||||||
|
val encrypted_content = parts[3]
|
||||||
|
Log.i("Decrypt schema", "Give data: $encrypted_content")
|
||||||
|
|
||||||
|
this.decrypted_raw = CryptoOperations().decrypt(
|
||||||
|
encrypted_content, passphrase,
|
||||||
|
iv.toByteArray(), salt.toByteArray())
|
||||||
|
|
||||||
|
return this.decrypted_raw
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will take all defined variables and converts them into the QR-Code schema.
|
||||||
|
*
|
||||||
|
* @param compress_values Takes an string array where a entry can be `website_url` in case of an URL or
|
||||||
|
* the key of an optional field. For a security question take the question as key.
|
||||||
|
*/
|
||||||
|
fun build(compress_values: Array<String>, passphrase: String): String {
|
||||||
|
/* First phase is to construct the encrypted data. */
|
||||||
|
val session_key = CryptoOperations().nextString(10) // used for
|
||||||
|
var used_compression: Boolean = false
|
||||||
|
|
||||||
|
var url_copy = website_url
|
||||||
|
if (compress_values.contains("website_url")) {
|
||||||
|
used_compression = true
|
||||||
|
val email_key = CryptoOperations().nextString(4)
|
||||||
|
this.db?.add(session_key, Compression(email_key, website_url, null, null))
|
||||||
|
url_copy = "\$$email_key"
|
||||||
|
}
|
||||||
|
var encrypted_content: String =
|
||||||
|
"${this.title}|${this.username}|${this.password}|${this.email}|${url_copy}|"
|
||||||
|
|
||||||
|
// Loop thought optional/custom fields
|
||||||
|
for (i in this.custom) {
|
||||||
|
if (compress_values.contains(i.key)) {
|
||||||
|
used_compression = true
|
||||||
|
var key_key = CryptoOperations().nextString(4)
|
||||||
|
var key_value = CryptoOperations().nextString(4)
|
||||||
|
|
||||||
|
this.db?.add(session_key,
|
||||||
|
Compression(key_key, i.key, null, null))
|
||||||
|
this.db?.add(session_key,
|
||||||
|
Compression(key_value, i.value, null, null))
|
||||||
|
|
||||||
|
encrypted_content += "(\$$key_key)\$$key_value|"
|
||||||
|
} else {
|
||||||
|
encrypted_content += "(${i.key})${i.value}|"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop thought security questions
|
||||||
|
for (i in this.question_awnser) {
|
||||||
|
used_compression = true
|
||||||
|
if (compress_values.contains(i.key)) {
|
||||||
|
var key_key = CryptoOperations().nextString(4)
|
||||||
|
var key_value = CryptoOperations().nextString(4)
|
||||||
|
|
||||||
|
this.db?.add(session_key,
|
||||||
|
Compression(key_key, i.key, null, null))
|
||||||
|
this.db?.add(session_key,
|
||||||
|
Compression(key_value, i.value, null, null))
|
||||||
|
|
||||||
|
encrypted_content += "()\$$key_key%\$$key_value|"
|
||||||
|
} else {
|
||||||
|
encrypted_content += "()${i.key}%${i.value}|"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encrypted_content = encrypted_content.dropLast(1)
|
||||||
|
if (used_compression) encrypted_content = "%$session_key%$encrypted_content"
|
||||||
|
|
||||||
|
Log.i("QR-Code Builder", "Constructed encrypted content: $encrypted_content")
|
||||||
|
|
||||||
|
// Now, let's encrypt that
|
||||||
|
val enc = runBlocking {
|
||||||
|
CryptoOperations().encrypt(passphrase, encrypted_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make schema version dynamic (currently hardcoded)
|
||||||
|
var final = "op1:" +
|
||||||
|
"${String(enc.iv)}:" +
|
||||||
|
"${String(enc.salt)}:" +
|
||||||
|
enc.result
|
||||||
|
.replace("\n", "")
|
||||||
|
|
||||||
|
Log.i("QR-Code Builder", "Returning final result: $final")
|
||||||
|
|
||||||
|
this.raw = final
|
||||||
|
|
||||||
|
return final
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
66
app/src/main/java/com/github/mondei1/offpass/TextInput.kt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package com.github.mondei1.offpass
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import kotlinx.android.synthetic.main.fragment_text_input.*
|
||||||
|
|
||||||
|
// TODO: Rename parameter arguments, choose names that match
|
||||||
|
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
|
||||||
|
private const val ARG_HEAD = "param1"
|
||||||
|
private const val ARG_TITLE = "param2"
|
||||||
|
private const val ARG_TITLE_SIZE = "param3"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple [Fragment] subclass.
|
||||||
|
* Use the [TextInput.newInstance] factory method to
|
||||||
|
* create an instance of this fragment.
|
||||||
|
*/
|
||||||
|
class TextInput : Fragment() {
|
||||||
|
// TODO: Rename and change types of parameters
|
||||||
|
private var arg_head: String? = null
|
||||||
|
private var arg_title: String? = null
|
||||||
|
private var arg_title_size: String? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
arguments?.let {
|
||||||
|
arg_title = it.getString(ARG_TITLE)
|
||||||
|
arg_head = it.getString(ARG_HEAD)
|
||||||
|
arg_title_size = it.getString(ARG_TITLE_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
return inflater.inflate(R.layout.fragment_text_input, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
head.text = arg_head
|
||||||
|
title_.hint = arg_title
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Use this factory method to create a new instance of
|
||||||
|
* this fragment using the provided parameters.
|
||||||
|
*/
|
||||||
|
// TODO: Rename and change types and number of parameters
|
||||||
|
@JvmStatic
|
||||||
|
fun newInstance(head: String, title: String, title_size: String) =
|
||||||
|
TextInput().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(ARG_HEAD, head)
|
||||||
|
putString(ARG_TITLE, title)
|
||||||
|
putString(ARG_TITLE_SIZE, title_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
app/src/main/java/com/github/mondei1/offpass/ViewActivity.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package com.github.mondei1.offpass
|
||||||
|
|
||||||
|
import android.R.attr.key
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.ActionBar
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.method.HideReturnsTransformationMethod
|
||||||
|
import android.text.method.PasswordTransformationMethod
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import dev.turingcomplete.kotlinonetimepassword.GoogleAuthenticator
|
||||||
|
import kotlinx.android.synthetic.main.activity_create.*
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.*
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.back
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.email_input
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.fa_input
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.fa_label
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.fa_progress
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.password_hide
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.password_input
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.title_label
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.url_input
|
||||||
|
import kotlinx.android.synthetic.main.activity_view.username_input
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
|
||||||
|
class ViewActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
var fa_coroutine: Job? = null
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
val i = Intent(this, MainActivity::class.java)
|
||||||
|
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
startActivity(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setContentView(view: View?) {
|
||||||
|
// Prevent other apps from making screenshots or to record Offpass.
|
||||||
|
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_view)
|
||||||
|
|
||||||
|
val qrSchema: QRSchema = QRSchema(this)
|
||||||
|
|
||||||
|
back.setOnClickListener {
|
||||||
|
val i = Intent(this, MainActivity::class.java)
|
||||||
|
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
startActivity(i)
|
||||||
|
}
|
||||||
|
password_hide.setOnClickListener {
|
||||||
|
if (password_input.transformationMethod == HideReturnsTransformationMethod.getInstance()) {
|
||||||
|
password_input.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||||
|
password_input.text = "123456"
|
||||||
|
password_hide.setImageResource(R.drawable.ic_visibility_on)
|
||||||
|
} else {
|
||||||
|
password_input.transformationMethod = HideReturnsTransformationMethod.getInstance()
|
||||||
|
password_input.text = qrSchema.password
|
||||||
|
password_hide.setImageResource(R.drawable.ic_visibility_off)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qrSchema.decrypted_raw = intent.getStringExtra("decrypted_raw")!!
|
||||||
|
qrSchema.parse(this)
|
||||||
|
|
||||||
|
title_label.text = qrSchema.title
|
||||||
|
username_input.setText(qrSchema.username, TextView.BufferType.EDITABLE)
|
||||||
|
password_input.setText("123456", TextView.BufferType.EDITABLE)
|
||||||
|
email_input.setText(qrSchema.email, TextView.BufferType.EDITABLE)
|
||||||
|
url_input.setText(qrSchema.website_url, TextView.BufferType.EDITABLE)
|
||||||
|
|
||||||
|
if (qrSchema.custom.containsKey("2fa")) {
|
||||||
|
val fa_uri = Uri.parse(qrSchema.custom.get("2fa"))
|
||||||
|
val fa_generator = GoogleAuthenticator(base32secret = fa_uri.getQueryParameter("secret").toString())
|
||||||
|
fa_label.text = "${fa_uri.path?.replace("/", "")} (${fa_uri.getQueryParameter("issuer")})"
|
||||||
|
fa_progress.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fa_layout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
fa_coroutine?.cancel(CancellationException("Activity is stopping"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.github.mondei1.offpass.entities
|
||||||
|
|
||||||
|
class Compression {
|
||||||
|
var key: String = "DEFAULT"
|
||||||
|
var value: String? = null
|
||||||
|
var iv: ByteArray? = null
|
||||||
|
var salt: ByteArray? = null
|
||||||
|
|
||||||
|
constructor(key: String, value: String, iv: ByteArray?, salt: ByteArray?) {
|
||||||
|
this.key = key
|
||||||
|
this.value = value
|
||||||
|
this.iv = iv
|
||||||
|
this.salt = salt
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.github.mondei1.offpass.entities
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import com.github.mondei1.offpass.CryptoOperations
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
class CompressionHelper(context: Context, factory: SQLiteDatabase.CursorFactory?):
|
||||||
|
SQLiteOpenHelper(context, DATABASE_NAME, factory, DATABASE_VERSION) {
|
||||||
|
|
||||||
|
private fun init_db(db: SQLiteDatabase) {
|
||||||
|
//db.execSQL("DROP TABLE $TABLE_NAME")
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `$TABLE_NAME` " +
|
||||||
|
"(`$COLUMN_KEY` VARCHAR(4) PRIMARY KEY, `$COLUMN_VALUE` TEXT, `$COLUMN_IV` BLOB," +
|
||||||
|
" `$COLUMN_SALT` BLOB)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
|
this.init_db(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpen(db: SQLiteDatabase) {
|
||||||
|
super.onOpen(db)
|
||||||
|
this.init_db(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||||||
|
// Do nothing. We don't want to delete any sensitive values.
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(session_key: String, compression: Compression) {
|
||||||
|
val encrypted_result = GlobalScope.async {
|
||||||
|
CryptoOperations().encrypt(session_key, compression.value!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
val res = encrypted_result.await()
|
||||||
|
writableDatabase.execSQL("INSERT INTO $TABLE_NAME (`$COLUMN_KEY`, `$COLUMN_VALUE`, `$COLUMN_IV`, `$COLUMN_SALT`) " +
|
||||||
|
"VALUES (?, ?, ?, ?)",
|
||||||
|
arrayOf(compression.key, res.result, res.iv, res.salt))
|
||||||
|
Log.d("Compression Helper", "Add key ${compression.key} = $res to database.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The encrypted content and the IV used for encryption of a specified key.
|
||||||
|
* The returned array looks like that: [key, encrypted_value, iv]
|
||||||
|
*/
|
||||||
|
suspend fun get(session_key: String, key: String): String? {
|
||||||
|
var cursor = this.readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME WHERE `key`=?", arrayOf(key))
|
||||||
|
var list: ArrayList<String> = ArrayList<String>()
|
||||||
|
var salt: ByteArray = ByteArray(16)
|
||||||
|
var iv: ByteArray = ByteArray(16)
|
||||||
|
|
||||||
|
with(cursor) {
|
||||||
|
while (moveToNext()) {
|
||||||
|
list.add(getString(getColumnIndex(COLUMN_VALUE)))
|
||||||
|
iv = getBlob(getColumnIndex(COLUMN_IV))
|
||||||
|
salt = getBlob(getColumnIndex(COLUMN_SALT))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val decrypted_result = GlobalScope.async {
|
||||||
|
// TODO: Is a list even required?
|
||||||
|
CryptoOperations().decrypt(list[0], session_key, iv, salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted_result.await().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATABASE_VERSION = 3
|
||||||
|
private val DATABASE_NAME = "compression.db"
|
||||||
|
val TABLE_NAME = "compression"
|
||||||
|
val COLUMN_KEY = "key"
|
||||||
|
val COLUMN_VALUE = "value"
|
||||||
|
val COLUMN_SALT = ""
|
||||||
|
val COLUMN_IV = "iv"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/src/main/res/drawable-anydpi/baseline_sd_storage_24.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="32dp"
|
||||||
|
android:height="32dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:pathData="M18,2h-8L4.02,8 4,20c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM12,8h-2L10,4h2v4zM15,8h-2L13,4h2v4zM18,8h-2L16,4h2v4z" />
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable-anydpi/ic_compress.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- drawable/zip_box_outline.xml -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="48dp"
|
||||||
|
android:width="48dp"
|
||||||
|
android:viewportWidth="26"
|
||||||
|
android:viewportHeight="26">
|
||||||
|
<path
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:pathData="M12 17V15H14V17H12M14 13V11H12V13H14M14 9V7H12V9H14M10 11H12V9H10V11M10 15H12V13H10V15M21 5V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V5C3 3.9 3.9 3 5 3H19C20.1 3 21 3.9 21 5M19 5H12V7H10V5H5V19H19V5Z" />
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable-anydpi/printer.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- drawable/printer.xml -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000"
|
||||||
|
android:pathData="M18,3H6V7H18M19,12A1,1 0 0,1 18,11A1,1 0 0,1 19,10A1,1 0 0,1 20,11A1,1 0 0,1 19,12M16,19H8V14H16M19,8H5A3,3 0 0,0 2,11V17H6V21H18V17H22V11A3,3 0 0,0 19,8Z" />
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 193 B |
BIN
app/src/main/res/drawable-hdpi/baseline_settings_white_36.png
Normal file
|
After Width: | Height: | Size: 627 B |
BIN
app/src/main/res/drawable-hdpi/baseline_visibility_black_36.png
Normal file
|
After Width: | Height: | Size: 662 B |
BIN
app/src/main/res/drawable-hdpi/baseline_visibility_black_48.png
Normal file
|
After Width: | Height: | Size: 819 B |
BIN
app/src/main/res/drawable-hdpi/ic_add.png
Normal file
|
After Width: | Height: | Size: 1011 B |
BIN
app/src/main/res/drawable-hdpi/ic_autorenew.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_content_copy.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_refresh.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_remove.png
Normal file
|
After Width: | Height: | Size: 526 B |
BIN
app/src/main/res/drawable-hdpi/ic_visibility_off.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_visibility_on.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 159 B |
BIN
app/src/main/res/drawable-mdpi/baseline_settings_white_36.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
app/src/main/res/drawable-mdpi/baseline_visibility_black_36.png
Normal file
|
After Width: | Height: | Size: 482 B |
BIN
app/src/main/res/drawable-mdpi/baseline_visibility_black_48.png
Normal file
|
After Width: | Height: | Size: 591 B |
BIN
app/src/main/res/drawable-mdpi/ic_add.png
Normal file
|
After Width: | Height: | Size: 395 B |
BIN
app/src/main/res/drawable-mdpi/ic_autorenew.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_content_copy.png
Normal file
|
After Width: | Height: | Size: 914 B |
BIN
app/src/main/res/drawable-mdpi/ic_refresh.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_remove.png
Normal file
|
After Width: | Height: | Size: 233 B |
BIN
app/src/main/res/drawable-mdpi/ic_visibility_off.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_visibility_on.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 206 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_settings_white_36.png
Normal file
|
After Width: | Height: | Size: 788 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_visibility_black_36.png
Normal file
|
After Width: | Height: | Size: 819 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_visibility_black_48.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_add.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_autorenew.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_content_copy.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_refresh.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_remove.png
Normal file
|
After Width: | Height: | Size: 664 B |
BIN
app/src/main/res/drawable-xhdpi/ic_visibility_off.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_visibility_on.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 271 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_settings_white_36.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_add.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_autorenew.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_content_copy.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_refresh.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_remove.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_visibility_off.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_visibility_on.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 276 B |
BIN
app/src/main/res/drawable-xxxhdpi/baseline_settings_white_36.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_add.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_autorenew.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_content_copy.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_refresh.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_remove.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_visibility_off.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_visibility_on.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
30
app/src/main/res/layout-v28/fragment_text_input.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".TextInput">
|
||||||
|
|
||||||
|
<!-- TODO: Update blank fragment layout -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/head"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:textColor="@color/colorPrimaryDark"
|
||||||
|
android:text="@string/text_input_head_undefined" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/editTextTextPersonName"
|
||||||
|
style="@style/Widget.AppCompat.EditText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:ems="15"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textFontWeight="600"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:hint="@string/text_input_title_undefined" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
318
app/src/main/res/layout/activity_create.xml
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/AppTheme"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
tools:context=".CreateActivity">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/colorPrimaryDark"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/print_button"
|
||||||
|
style="@style/Widget.AppCompat.Button.Colored"
|
||||||
|
android:layout_width="88dp"
|
||||||
|
android:layout_height="55dp"
|
||||||
|
android:layout_margin="0dp"
|
||||||
|
android:layout_marginStart="64dp"
|
||||||
|
android:backgroundTint="#F8F8F8"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:src="@drawable/printer"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:textSize="24sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/back"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:src="@drawable/baseline_arrow_forward_white_36"
|
||||||
|
android:text=""
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/settings"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="31dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="72dp"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
android:scaleX="1"
|
||||||
|
android:scaleY="1"
|
||||||
|
android:src="@drawable/baseline_settings_white_36"
|
||||||
|
android:text=""
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/settings2"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="38dp"
|
||||||
|
android:layout_height="38dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="116dp"
|
||||||
|
android:layout_marginBottom="3dp"
|
||||||
|
android:src="@drawable/ic_compress"
|
||||||
|
android:text=""
|
||||||
|
android:textColor="#000000"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1.0" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/scroll_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="56dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/url_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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="97dp"
|
||||||
|
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="0dp"
|
||||||
|
android:layout_height="81dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:ems="10"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:hint="Secure password"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textSize="28sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/password_label"
|
||||||
|
app:layout_constraintWidth_percent="0.6" />
|
||||||
|
|
||||||
|
<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_constraintEnd_toStartOf="@+id/password_random"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/password_input"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/password_input"
|
||||||
|
app:layout_constraintVertical_bias="0.48000002"
|
||||||
|
app:srcCompat="@drawable/ic_visibility_on" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/password_random"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="15dp"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:tint="#000000"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/password_input"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/password_hide"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/password_input"
|
||||||
|
app:srcCompat="@drawable/ic_autorenew" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/fa_progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="379dp"
|
||||||
|
android:layout_height="15dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:max="30"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/fa_input" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/fa_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:text="Two factor authentication"
|
||||||
|
android:textSize="18sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/url_input" />
|
||||||
|
|
||||||
|
<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="#00FFFFFF"
|
||||||
|
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" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button"
|
||||||
|
style="@style/Widget.AppCompat.Button.Colored"
|
||||||
|
android:layout_width="256dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:background="@color/colorPrimaryDark"
|
||||||
|
android:text="Add more"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/fa_progress" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
171
app/src/main/res/layout/activity_generator.xml
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:context=".GeneratorActivity">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="350dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="256dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginStart="80dp"
|
||||||
|
android:layout_marginTop="100dp"
|
||||||
|
android:layout_marginEnd="80dp"
|
||||||
|
android:progress="2"
|
||||||
|
android:min="0"
|
||||||
|
android:max="4"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/random_string"
|
||||||
|
android:layout_width="201dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="80dp"
|
||||||
|
android:layout_marginEnd="80dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:text="D%g$MA0H8d"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textSize="30sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/progressBar"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/refresh_button"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="#00FFFFFF"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/progressBar"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/random_string"
|
||||||
|
app:srcCompat="@drawable/ic_refresh" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/copy_button"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="#00FFFFFF"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/progressBar"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/random_string"
|
||||||
|
app:srcCompat="@drawable/ic_content_copy" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/password_length_container"
|
||||||
|
android:layout_width="142dp"
|
||||||
|
android:layout_height="45dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.498"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/progressBar">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/subtract_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="#00FFFFFF"
|
||||||
|
app:srcCompat="@drawable/ic_remove" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/length_label"
|
||||||
|
android:layout_width="45dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fontFamily="sans-serif-light"
|
||||||
|
android:gravity="center_horizontal|center_vertical"
|
||||||
|
android:text="0"
|
||||||
|
android:textColor="@color/colorPrimaryDark"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/add_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="#00FFFFFF"
|
||||||
|
app:srcCompat="@drawable/ic_add" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/AZ"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="A-Z"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/password_length_container" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/az"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="a-z"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/AZ" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/zero_nine"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="0-9"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/az" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/special"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="!"#$%'()*+"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/zero_nine" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/emojis"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="😃👽😺👋"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/special" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
app:srcCompat="@android:drawable/ic_menu_set_as" />
|
app:srcCompat="@android:drawable/ic_menu_set_as" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/imageButton2"
|
android:id="@+id/add_button"
|
||||||
style="@style/Widget.AppCompat.Button.Borderless"
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
android:layout_height="120dp"
|
android:layout_height="120dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -90,12 +90,13 @@
|
|||||||
android:id="@+id/textView"
|
android:id="@+id/textView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
android:layout_marginBottom="5dp"
|
android:layout_marginBottom="5dp"
|
||||||
android:fontFamily="@font/righteous"
|
android:fontFamily="@font/righteous"
|
||||||
android:text="ALPHA"
|
android:text="ALPHA"
|
||||||
android:textColor="@color/colorPrimary"
|
android:textColor="@color/colorPrimary"
|
||||||
android:textSize="36sp"
|
android:textSize="36sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
tools:layout_editor_absoluteX="291dp" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
81
app/src/main/res/layout/activity_passphrase.xml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/colorPrimaryDark"
|
||||||
|
tools:context=".PassphraseActivity">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/passphrase_input"
|
||||||
|
android:layout_width="286dp"
|
||||||
|
android:layout_height="61dp"
|
||||||
|
android:layout_marginStart="88dp"
|
||||||
|
android:layout_marginEnd="89dp"
|
||||||
|
android:backgroundTint="#757575"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="Passphrase"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:letterSpacing="0.2"
|
||||||
|
android:paddingBottom="15dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:textColorHint="#757575"
|
||||||
|
android:textSize="20dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/decrypt_button"
|
||||||
|
style="@style/Widget.AppCompat.Button"
|
||||||
|
android:layout_width="152dp"
|
||||||
|
android:layout_height="59dp"
|
||||||
|
android:layout_marginStart="161dp"
|
||||||
|
android:layout_marginTop="48dp"
|
||||||
|
android:layout_marginEnd="162dp"
|
||||||
|
android:background="#00FFFFFF"
|
||||||
|
android:backgroundTint="#001C1C1C"
|
||||||
|
android:hapticFeedbackEnabled="false"
|
||||||
|
android:text="Decrypt"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:textSize="18sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.484"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/passphrase_input" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/back"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="163dp"
|
||||||
|
android:layout_marginEnd="160dp"
|
||||||
|
android:layout_marginBottom="30dp"
|
||||||
|
android:fontFamily="sans-serif-light"
|
||||||
|
android:text="Back"
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/failed"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="180dp"
|
||||||
|
android:layout_marginEnd="180dp"
|
||||||
|
android:layout_marginBottom="40dp"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="Wrong Passphrase"
|
||||||
|
android:textColor="#E53935"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/passphrase_input"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
230
app/src/main/res/layout/activity_view.xml
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/AppTheme"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
tools:context=".ViewActivity">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/colorPrimaryDark"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:titleMargin="0dp">
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/back"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:rotation="180"
|
||||||
|
android:src="@drawable/baseline_arrow_forward_white_36"
|
||||||
|
android:text="" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:text="TITLE"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/scrollView2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/fa_layout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/url_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
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: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_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/password_input"
|
||||||
|
android:layout_width="288dp"
|
||||||
|
android:layout_height="68dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="@color/colorPrimaryDark"
|
||||||
|
android:textSize="42sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/password_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
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: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" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
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:singleLine="true"
|
||||||
|
android:textSize="24sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/url_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" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/password_hide"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="28dp"
|
||||||
|
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.45"
|
||||||
|
app:srcCompat="@drawable/ic_visibility_on" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/fa_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/scrollView2">
|
||||||
|
|
||||||
|
<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_marginTop="-10dp"
|
||||||
|
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>
|
||||||
42
app/src/main/res/layout/dialogpassphrase.xml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@color/colorPrimaryDark"
|
||||||
|
android:paddingLeft="50dp"
|
||||||
|
android:paddingRight="50dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/passphrase_input"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:backgroundTint="@color/colorPrimary"
|
||||||
|
android:textColorHint="@color/colorPrimary"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:hint="Enter passphrase"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/passphrase2_input"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:backgroundTint="@color/colorPrimary"
|
||||||
|
android:textColorHint="@color/colorPrimary"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:hint="Repeat passphrase"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/hint_input"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:backgroundTint="@color/colorPrimary"
|
||||||
|
android:textColorHint="@color/colorPrimary"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginBottom="30dp"
|
||||||
|
android:maxLength="32"
|
||||||
|
android:hint="Enter hint (optional)" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
41
app/src/main/res/layout/fragment_text_input.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".TextInput">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/head"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:text="@string/text_input_head_undefined"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
|
||||||
|
android:textColor="@color/colorPrimaryDark"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/title_"
|
||||||
|
style="@style/Widget.AppCompat.EditText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:ems="15"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:hint="@string/text_input_title_undefined" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/imageButton2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="350dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:src="@drawable/baseline_visibility_black_48" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
183048
app/src/main/res/raw/passwordlist.txt
Normal file
80
app/src/main/res/raw/print.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Head is not important since
|
||||||
|
we will only print the body-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
max-height: 100vh;
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
border: 3px rgba(0, 0, 0, 0) solid;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-top: 8rem;
|
||||||
|
font-size: 36pt;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 24pt;
|
||||||
|
}
|
||||||
|
#hint_title {
|
||||||
|
margin-top: 10rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-top: 5rem;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background stripes */
|
||||||
|
#bg img {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="content">
|
||||||
|
<h1>OffPass</h1>
|
||||||
|
<p>Created on $DATE</p>
|
||||||
|
|
||||||
|
<p id="hint_title">Password hint</p>
|
||||||
|
<p>$HINT</p>
|
||||||
|
|
||||||
|
<img src="data:image/png;base64, $QRCODE">
|
||||||
|
<div id="bg">
|
||||||
|
<img src="print_background.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
app/src/main/res/values-round/strings.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<resources></resources>
|
||||||
13
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!--
|
||||||
|
Because the window insets on round devices are larger than 15dp, this padding only applies
|
||||||
|
to square screens.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This padding applies to both square and round screens. The total padding between the buttons
|
||||||
|
and the window insets is box_inset_layout_padding (above variable) on square screens and
|
||||||
|
inner_frame_layout_padding (below variable) on round screens.
|
||||||
|
-->
|
||||||
|
</resources>
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">OffPass</string>
|
<string name="app_name">OffPass</string>
|
||||||
|
<!--
|
||||||
|
This string is used for square devices and overridden by hello_world in
|
||||||
|
values-round/strings.xml for round devices.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<string name="text_input_head_undefined">HEAD UNDEFINED</string>
|
||||||
|
<string name="text_input_title_undefined">TITLE UNDEFINED</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,10 +1,29 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">#fff</item>
|
||||||
|
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="PassphraseDialog" parent="@android:style/Theme.Material.Dialog">
|
||||||
|
<item name="android:textColor">@color/colorPrimary</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
|
<item name="android:backgroundDimAmount">0.9</item>
|
||||||
|
<item name="android:background">@color/colorPrimaryDark</item>
|
||||||
|
</style>
|
||||||
|
<style name="ErrorDialog" parent="@android:style/Theme.Material.Dialog">
|
||||||
|
<item name="android:textColor">@color/colorPrimary</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
|
<item name="android:background">@android:color/holo_red_light</item>
|
||||||
|
</style>
|
||||||
|
<style name="PassphraseGenerator" parent="@android:style/Theme.Material.Dialog">
|
||||||
|
<item name="android:paddingTop">0dp</item>
|
||||||
|
<item name="android:layout_marginTop">0dp</item>
|
||||||
|
<item name="android:textColor">@color/colorPrimaryDark</item>
|
||||||
|
<item name="background">@color/colorPrimary</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,3 +2,4 @@ Credits
|
|||||||
===================================
|
===================================
|
||||||
Icons made by photo3idea_studio (https://www.flaticon.com/de/autoren/photo3idea-studio) from www.flaticon.com
|
Icons made by photo3idea_studio (https://www.flaticon.com/de/autoren/photo3idea-studio) from www.flaticon.com
|
||||||
Icons made by xnimrodx (https://www.flaticon.com/de/autoren/xnimrodx) from www.flaticon.com
|
Icons made by xnimrodx (https://www.flaticon.com/de/autoren/xnimrodx) from www.flaticon.com
|
||||||
|
Icons made by Michael Irigoyen (http://twitter.com/mririgo)
|
||||||