forked from thijooree/android
117 lines
3.9 KiB
Kotlin
117 lines
3.9 KiB
Kotlin
package sh.sar.basedbank.util
|
|
|
|
import android.net.Uri
|
|
import android.util.Base64
|
|
|
|
data class OtpEntry(val name: String, val issuer: String, val secret: String)
|
|
|
|
object OtpauthParser {
|
|
|
|
fun parse(raw: String): List<OtpEntry> = when {
|
|
raw.startsWith("otpauth-migration://") -> parseMigration(raw)
|
|
raw.startsWith("otpauth://") -> parseStandard(raw)?.let { listOf(it) } ?: emptyList()
|
|
else -> emptyList()
|
|
}
|
|
|
|
private fun parseStandard(raw: String): OtpEntry? {
|
|
val uri = Uri.parse(raw)
|
|
val secret = uri.getQueryParameter("secret") ?: return null
|
|
val issuer = uri.getQueryParameter("issuer") ?: ""
|
|
val label = uri.path?.trimStart('/') ?: ""
|
|
val name = if (':' in label) label.substringAfter(':').trim() else label
|
|
return OtpEntry(name, issuer, secret.uppercase())
|
|
}
|
|
|
|
private fun parseMigration(raw: String): List<OtpEntry> {
|
|
val data = Uri.parse(raw).getQueryParameter("data") ?: return emptyList()
|
|
val bytes = try { Base64.decode(data, Base64.DEFAULT) } catch (_: Exception) { return emptyList() }
|
|
val reader = ProtobufReader(bytes)
|
|
val entries = mutableListOf<OtpEntry>()
|
|
while (reader.hasMore()) {
|
|
val tag = reader.readVarint().toInt()
|
|
val fieldNum = tag ushr 3
|
|
val wireType = tag and 0x7
|
|
if (fieldNum == 1 && wireType == 2) {
|
|
parseOtpParameters(reader.readBytes())?.let { entries.add(it) }
|
|
} else {
|
|
reader.skip(wireType)
|
|
}
|
|
}
|
|
return entries
|
|
}
|
|
|
|
private fun parseOtpParameters(bytes: ByteArray): OtpEntry? {
|
|
val reader = ProtobufReader(bytes)
|
|
var secret: ByteArray? = null
|
|
var name = ""
|
|
var issuer = ""
|
|
var type = 2 // default to TOTP
|
|
while (reader.hasMore()) {
|
|
val tag = reader.readVarint().toInt()
|
|
val fieldNum = tag ushr 3
|
|
val wireType = tag and 0x7
|
|
when (fieldNum) {
|
|
1 -> secret = reader.readBytes()
|
|
2 -> name = String(reader.readBytes(), Charsets.UTF_8)
|
|
3 -> issuer = String(reader.readBytes(), Charsets.UTF_8)
|
|
6 -> type = reader.readVarint().toInt()
|
|
else -> reader.skip(wireType)
|
|
}
|
|
}
|
|
if (type == 1) return null // skip HOTP
|
|
val secretBase32 = base32Encode(secret ?: return null)
|
|
return OtpEntry(name, issuer, secretBase32)
|
|
}
|
|
|
|
private fun base32Encode(bytes: ByteArray): String {
|
|
val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
|
|
val sb = StringBuilder()
|
|
var buffer = 0
|
|
var bitsLeft = 0
|
|
for (b in bytes) {
|
|
buffer = (buffer shl 8) or (b.toInt() and 0xFF)
|
|
bitsLeft += 8
|
|
while (bitsLeft >= 5) {
|
|
bitsLeft -= 5
|
|
sb.append(alphabet[(buffer ushr bitsLeft) and 0x1F])
|
|
}
|
|
}
|
|
if (bitsLeft > 0) sb.append(alphabet[(buffer shl (5 - bitsLeft)) and 0x1F])
|
|
return sb.toString()
|
|
}
|
|
|
|
private class ProtobufReader(private val bytes: ByteArray) {
|
|
private var pos = 0
|
|
|
|
fun hasMore() = pos < bytes.size
|
|
|
|
fun readVarint(): Long {
|
|
var result = 0L
|
|
var shift = 0
|
|
while (pos < bytes.size) {
|
|
val b = bytes[pos++].toInt() and 0xFF
|
|
result = result or ((b and 0x7F).toLong() shl shift)
|
|
if (b and 0x80 == 0) break
|
|
shift += 7
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun readBytes(): ByteArray {
|
|
val len = readVarint().toInt()
|
|
val data = bytes.copyOfRange(pos, pos + len)
|
|
pos += len
|
|
return data
|
|
}
|
|
|
|
fun skip(wireType: Int) {
|
|
when (wireType) {
|
|
0 -> readVarint()
|
|
1 -> pos += 8
|
|
2 -> readBytes()
|
|
5 -> pos += 4
|
|
}
|
|
}
|
|
}
|
|
}
|