forked from thijooree/android
Add support for otpauth:// and otpauth-migration:// QR scan during login
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user