Files
KeePassDX/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt
2020-06-23 15:53:13 +02:00

281 lines
8.7 KiB
Kotlin

/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.otp
import com.kunzisoft.keepass.model.OtpModel
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.binary.Hex
import java.nio.charset.Charset
import java.util.*
import java.util.regex.Pattern
data class OtpElement(var otpModel: OtpModel = OtpModel()) {
var type
get() = otpModel.type
set(value) {
otpModel.type = value
if (type == OtpType.HOTP) {
if (!OtpTokenType.getHotpTokenTypeValues().contains(tokenType))
tokenType = OtpTokenType.RFC4226
}
if (type == OtpType.TOTP) {
if (!OtpTokenType.getTotpTokenTypeValues().contains(tokenType))
tokenType = OtpTokenType.RFC6238
}
}
var tokenType
get() = otpModel.tokenType
set(value) {
otpModel.tokenType = value
when (tokenType) {
OtpTokenType.RFC4226 -> {
otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM
otpModel.digits = TokenCalculator.OTP_DEFAULT_DIGITS
otpModel.counter = TokenCalculator.HOTP_INITIAL_COUNTER
}
OtpTokenType.RFC6238 -> {
otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM
otpModel.digits = TokenCalculator.OTP_DEFAULT_DIGITS
otpModel.period = TokenCalculator.TOTP_DEFAULT_PERIOD
}
OtpTokenType.STEAM -> {
otpModel.algorithm = TokenCalculator.OTP_DEFAULT_ALGORITHM
otpModel.digits = TokenCalculator.STEAM_DEFAULT_DIGITS
otpModel.period = TokenCalculator.TOTP_DEFAULT_PERIOD
}
}
}
var name
get() = otpModel.name
set(value) {
otpModel.name = value
}
var issuer
get() = otpModel.issuer
set(value) {
otpModel.issuer = value
}
var secret
get() = otpModel.secret
private set(value) {
otpModel.secret = value
}
var counter
get() = otpModel.counter
@Throws(NumberFormatException::class)
set(value) {
otpModel.counter = if (isValidCounter(value)) {
value
} else {
TokenCalculator.HOTP_INITIAL_COUNTER
throw IllegalArgumentException()
}
}
var period
get() = otpModel.period
@Throws(NumberFormatException::class)
set(value) {
otpModel.period = if (isValidPeriod(value)) {
value
} else {
TokenCalculator.TOTP_DEFAULT_PERIOD
throw NumberFormatException()
}
}
var digits
get() = otpModel.digits
@Throws(NumberFormatException::class)
set(value) {
otpModel.digits = if (isValidDigits(value)) {
value
} else {
TokenCalculator.OTP_DEFAULT_DIGITS
throw NumberFormatException()
}
}
var algorithm
get() = otpModel.algorithm
set(value) {
otpModel.algorithm = value
}
@Throws(IllegalArgumentException::class)
fun setUTF8Secret(secret: String) {
if (secret.isNotEmpty())
otpModel.secret = secret.toByteArray(Charset.forName("UTF-8"))
else
throw IllegalArgumentException()
}
@Throws(IllegalArgumentException::class)
fun setHexSecret(secret: String) {
if (secret.isNotEmpty())
otpModel.secret = Hex.decodeHex(secret)
else
throw IllegalArgumentException()
}
fun getBase32Secret(): String {
return otpModel.secret?.let {
Base32().encodeAsString(it)
} ?: ""
}
@Throws(IllegalArgumentException::class)
fun setBase32Secret(secret: String) {
if (isValidBase32(secret))
otpModel.secret = Base32().decode(replaceBase32Chars(secret).toByteArray())
else
throw IllegalArgumentException()
}
@Throws(IllegalArgumentException::class)
fun setBase64Secret(secret: String) {
if (isValidBase64(secret))
otpModel.secret = Base64().decode(secret.toByteArray())
else
throw IllegalArgumentException()
}
val token: String
get() {
if (secret == null)
return ""
return when (type) {
OtpType.HOTP -> TokenCalculator.HOTP(secret, counter, digits, algorithm)
OtpType.TOTP -> when (tokenType) {
OtpTokenType.STEAM -> TokenCalculator.TOTP_Steam(secret, period, digits, algorithm)
else -> TokenCalculator.TOTP_RFC6238(secret, period, digits, algorithm)
}
}
}
val secondsRemaining: Int
get() = otpModel.period - (System.currentTimeMillis() / 1000 % otpModel.period).toInt()
fun shouldRefreshToken(): Boolean {
return secondsRemaining == otpModel.period
}
companion object {
const val MIN_HOTP_COUNTER = 1
const val MAX_HOTP_COUNTER = Long.MAX_VALUE
const val MIN_TOTP_PERIOD = 1
const val MAX_TOTP_PERIOD = 900
const val MIN_OTP_DIGITS = 4
const val MAX_OTP_DIGITS = 18
fun isValidCounter(counter: Long): Boolean {
return counter in MIN_HOTP_COUNTER..MAX_HOTP_COUNTER
}
fun isValidPeriod(period: Int): Boolean {
return period in MIN_TOTP_PERIOD..MAX_TOTP_PERIOD
}
fun isValidDigits(digits: Int): Boolean {
return digits in MIN_OTP_DIGITS..MAX_OTP_DIGITS
}
fun isValidBase32(secret: String): Boolean {
val secretChars = replaceBase32Chars(secret)
return secretChars.isNotEmpty() && checkBase32Secret(secretChars)
}
fun isValidBase64(secret: String): Boolean {
// TODO replace base 64 chars
return secret.isNotEmpty() && checkBase64Secret(secret)
}
fun replaceSpaceChars(parameter: String): String {
return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
}
fun replaceBase32Chars(parameter: String): String {
// Add 'A' at end if not Base32 length
var parameterNewSize = replaceSpaceChars(parameter.toUpperCase(Locale.ENGLISH))
while (parameterNewSize.length % 8 != 0) {
parameterNewSize += 'A'
}
return parameterNewSize
}
fun checkBase32Secret(secret: String): Boolean {
return (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secret))
}
fun checkBase64Secret(secret: String): Boolean {
return (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret))
}
}
}
enum class OtpType {
HOTP, // counter based
TOTP; // time based
}
enum class OtpTokenType {
RFC4226, // HOTP
RFC6238, // TOTP
// Proprietary
STEAM; // TOTP Steam
override fun toString(): String {
return when (this) {
STEAM -> "steam"
else -> super.toString()
}
}
companion object {
fun getFromString(tokenType: String): OtpTokenType {
return when (tokenType.toLowerCase(Locale.ENGLISH)) {
"s", "steam" -> STEAM
"hotp" -> RFC4226
else -> RFC6238
}
}
fun getTotpTokenTypeValues(getProprietaryElements: Boolean = true): Array<OtpTokenType> {
return if (getProprietaryElements)
arrayOf(RFC6238, STEAM)
else
arrayOf(RFC6238)
}
fun getHotpTokenTypeValues(): Array<OtpTokenType> {
return arrayOf(RFC4226)
}
}
}