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
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -34,12 +35,27 @@ repositories {
|
||||
|
||||
|
||||
dependencies {
|
||||
def room_version = "2.2.5"
|
||||
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
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 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
@@ -6,12 +6,20 @@
|
||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||
|
||||
<application
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:hardwareAccelerated="true"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:screenOrientation="portrait">
|
||||
@@ -21,6 +29,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<meta-data
|
||||
android:name="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.widget.Toast
|
||||
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.IntentResult
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -17,13 +21,22 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
var dbHandler = CompressionHelper(this, null)
|
||||
dbHandler.writableDatabase.execSQL("DELETE FROM `${CompressionHelper.TABLE_NAME}`;")
|
||||
|
||||
dbHandler.writableDatabase.close()
|
||||
|
||||
main_screen.setOnClickListener {
|
||||
IntentIntegrator(this)
|
||||
.setPrompt("Scan OffPass created QR-Code")
|
||||
.setBeepEnabled(false)
|
||||
.setBarcodeImageEnabled(false)
|
||||
.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)
|
||||
var result: IntentResult =
|
||||
IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
if (result != null) {
|
||||
if (result.contents == null) {
|
||||
Toast.makeText(this, "Cancelled", Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
Log.i("SCANNER", "Scanned: " + result.contents)
|
||||
Toast.makeText(this, "Scanned: " + result.contents, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
// TODO: Check if scanned QR-Code is an OffPass one.
|
||||
val intent: Intent = Intent(this, PassphraseActivity::class.java)
|
||||
intent.putExtra("raw", result.contents)
|
||||
startActivity(intent)
|
||||
//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" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/imageButton2"
|
||||
android:id="@+id/add_button"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_height="120dp"
|
||||
android:layout_weight="1"
|
||||
@@ -90,12 +90,13 @@
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:fontFamily="@font/righteous"
|
||||
android:text="ALPHA"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textSize="36sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:layout_editor_absoluteX="291dp" />
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</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>
|
||||
<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>
|
||||
@@ -1,10 +1,29 @@
|
||||
<resources>
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</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 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>
|
||||
@@ -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 xnimrodx (https://www.flaticon.com/de/autoren/xnimrodx) from www.flaticon.com
|
||||
Icons made by Michael Irigoyen (http://twitter.com/mririgo)
|
||||