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() /** * 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 } 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")) } }