Created
November 21, 2013 12:02
-
-
Save takumakei/7580417 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// TOTP.scala | |
// Copyright (c) 2013 TAKUMA Kei<[email protected]> | |
// 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)}") | |
} |
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
I don't know why others fail. Base32 should not matter because it comes from rfc 4648.
BTW i'm sorry for replying late.