package com.zeapo.pwdstore import android.net.Uri import java.io.ByteArrayOutputStream import java.io.UnsupportedEncodingException /** * A single entry in password store. */ class PasswordEntry(private val content: String) { val password: String val username: String? val totpSecret: String? val hotpSecret: String? val hotpCounter: Long? var extraContent: String? = null private set private var isIncremented = false @Throws(UnsupportedEncodingException::class) constructor(os: ByteArrayOutputStream) : this(os.toString("UTF-8")) init { val passContent = content.split("\n".toRegex(), 2).toTypedArray() password = passContent[0] totpSecret = findTotpSecret(content) hotpSecret = findHotpSecret(content) hotpCounter = findHotpCounter(content) extraContent = findExtraContent(passContent) username = findUsername() } fun hasExtraContent(): Boolean { return !extraContent.isNullOrEmpty() } fun hasUsername(): Boolean { return username != null } fun hasTotp(): Boolean { return totpSecret != null } fun hasHotp(): Boolean { return hotpSecret != null && hotpCounter != null } fun hotpIsIncremented(): Boolean { return isIncremented } fun incrementHotp() { content.split("\n".toRegex()).forEach { line -> if (line.startsWith("otpauth://hotp/")) { extraContent = extraContent?.replaceFirst("counter=[0-9]+".toRegex(), "counter=${hotpCounter!! + 1}") isIncremented = true } } } private fun findUsername(): String? { val extraLines = extraContent!!.split("\n".toRegex()) for (line in extraLines) { for (field in USERNAME_FIELDS) { if (line.toLowerCase().startsWith("$field:")) { return line.split(": *".toRegex(), 2).toTypedArray()[1] } } } return null } private fun findTotpSecret(decryptedContent: String): String? { decryptedContent.split("\n".toRegex()).forEach { line -> if (line.startsWith("otpauth://totp/")) { return Uri.parse(line).getQueryParameter("secret") } } return null } private fun findHotpSecret(decryptedContent: String): String? { decryptedContent.split("\n".toRegex()).forEach { line -> if (line.startsWith("otpauth://hotp/")) { return Uri.parse(line).getQueryParameter("secret") } } return null } private fun findHotpCounter(decryptedContent: String): Long? { decryptedContent.split("\n".toRegex()).forEach { line -> if (line.startsWith("otpauth://hotp/")) { return java.lang.Long.parseLong(Uri.parse(line).getQueryParameter("counter")!!) } } return null } private fun findExtraContent(passContent: Array): String { val extraContent = if (passContent.size > 1) passContent[1] else "" // if there is a HOTP URI, we must return the extra content with the counter incremented return if (hasHotp()) { extraContent.replaceFirst("counter=[0-9]+".toRegex(), "counter=" + java.lang.Long.toString(hotpCounter!!)) } else extraContent } companion object { private val USERNAME_FIELDS = arrayOf("login", "username") } }