Skip to content

Instantly share code, notes, and snippets.

@takumakei
Created November 21, 2013 12:02
Show Gist options
  • Save takumakei/7580417 to your computer and use it in GitHub Desktop.
Save takumakei/7580417 to your computer and use it in GitHub Desktop.
// TOTP.scala
// Copyright (c) 2013 TAKUMA Kei<takumakei@gmail.com>
// This software is released under the MIT License.
// http://opensource.org/licenses/MIT
package com.takumakei.totp
import scala.annotation.tailrec
object Base32 {
private val char2int: Map[Char, Int] = {
val chrs = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
def f(a: (Int, Map[Char, Int]), b: Char) = (a._1 + 1, a._2 + (b -> a._1))
chrs.foldLeft((0, Map.empty[Char, Int]))(f)._2
}
private val int2char = char2int.map(a => (a._2, a._1)).toMap
private def ignoreChrs(a: Char) = "- ".contains(a)
private def prettyString(a: List[Char]) = {
@tailrec
def f(a: List[Char], b: List[Char], c: List[String]): List[String] = {
a match {
case a :: as if b.size == 3 => f(as, Nil, (a :: b).reverse.mkString :: c)
case a :: as => f(as, a :: b, c)
case Nil if b.size != 0 => b.reverse.mkString :: c
case Nil => c
}
}
f(a, Nil, Nil).reverse.mkString("-")
}
def encode(a: Array[Byte]): String = {
case class A(acc: Int, len: Int, chrs: List[Char])
def f(a: A, b: Byte): A = g(A((a.acc << 8) + b, a.len + 8, a.chrs))
@tailrec
def g(a: A): A = if (a.len < 5) a else {
g(A(a.acc, a.len - 5, int2char(0x1f & (a.acc >>> (a.len - 5))) :: a.chrs))
}
prettyString(a.foldLeft(A(0, 0, Nil))(f).ensuring(_.len == 0, "bad length").chrs.reverse)
}
def decode(a: String): Array[Byte] = {
case class A(acc: Int, len: Int, bytes: List[Byte])
def f(a: A, b: Int): A = g(A((a.acc << 5) + b, a.len + 5, a.bytes))
@tailrec
def g(a: A): A = if (a.len < 8) a else {
g(A(a.acc, a.len - 8, (0xff & a.acc >>> (a.len - 8)).toByte :: a.bytes))
}
def h(a: Char) = char2int.get(a).getOrElse(throw new IllegalArgumentException("bad string for base32"))
a.filterNot(ignoreChrs).toUpperCase.map(h).foldLeft(A(0, 0, Nil))(f).ensuring(_.len == 0, "bad length").bytes.reverse.toArray
}
}
import java.nio.ByteBuffer
import java.security.SecureRandom
import scala.concurrent.duration._
import javax.crypto._
import javax.crypto.spec._
object TOTP {
def mkSeed() = {
val a = SecureRandom.getInstance("SHA1PRNG")
val s = new Array[Byte](10)
a.nextBytes(s)
Base32.encode(s)
}
def mkQRcodeURL(issuer: String, account: String, key: String) = {
s"http://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/${issuer}%3A${account}?issuer=${issuer}%26secret=${key}"
}
}
class TOTP(key: String, keyRegeneration: Duration = 30.seconds, keyLen: Int = 6) {
val mac = Mac.getInstance("HMACSHA1")
mac.init(new SecretKeySpec(Base32.decode(key), mac.getAlgorithm))
def generate(offset: Int): String = truncate(mac.doFinal(toBytes(timeStamp+offset)))
def timeStamp() = System.currentTimeMillis / keyRegeneration.toMillis
def toBytes(a: Long) = ByteBuffer.allocate(8).putLong(a).array
def truncate(a: Array[Byte]) = {
val offset = a(19) & 0xf
val s = a(offset + 0) & 0x7f
val t = a(offset + 1) & 0xff
val u = a(offset + 2) & 0xff
val v = a(offset + 3) & 0xff
val x = ((s << 24) | (t << 16) | (u << 8) | v) % math.pow(10, keyLen).toInt
"%%0%dd".format(keyLen).format(x)
}
}
object Main extends App {
val seed = Option(System.getProperty("TOTP.SAMPLE.KEY")).getOrElse {
val key = TOTP.mkSeed
System.setProperty("TOTP.SAMPLE.KEY", key)
key
}
println(s"seed: [${seed}](${TOTP.mkQRcodeURL("takumakei.com", "staff", seed)})")
val totp = new TOTP(seed)
for (offset <- Range.inclusive(-1, 1)) println(s"totp: ${totp.generate(offset)}")
}
@alexandernivanov
Copy link

Man, thanks a lot for this realization, it's the only one that actually matches Google Authenticator codes! The others fail for some reason; perhaps it's your custom Base32 code instead of Apache commons?

@takumakei
Copy link
Author

I don't know why others fail. Base32 should not matter because it comes from rfc 4648.
BTW i'm sorry for replying late.

@minisaw
Copy link

minisaw commented Jan 26, 2017

f(a, Nil, Nil).reverse.mkString("-")

this MUST read:

f(a, Nil, Nil).reverse.mkString

because most TOTP apps (non-mobile) take the provided key verbatim, and the "-" signs completely corrupt the key

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment