- User has to repeat his passphrase - Passwords can now be toggled - Error message on wrong passphrase on decryption
320 lines
14 KiB
Kotlin
320 lines
14 KiB
Kotlin
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 dev.turingcomplete.kotlinonetimepassword.GoogleAuthenticator
|
|
import kotlinx.android.synthetic.main.activity_create.*
|
|
import kotlinx.android.synthetic.main.dialogpassphrase.view.*
|
|
import kotlinx.coroutines.*
|
|
import java.io.ByteArrayOutputStream
|
|
import java.lang.IllegalArgumentException
|
|
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 {
|
|
return this.known_passwords.contains(password)
|
|
}
|
|
|
|
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 {
|
|
// 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()
|
|
}
|
|
|
|
|
|
}
|
|
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)
|
|
|
|
}
|
|
}
|
|
|
|
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"))
|
|
}
|
|
|
|
} |