Skip to content

Commit

Permalink
Inital commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ejisan committed Jun 18, 2016
0 parents commit ddec86c
Show file tree
Hide file tree
Showing 16 changed files with 705 additions and 0 deletions.
34 changes: 34 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/RUNNING_PID
/logs/
/project/*-shim.sbt
/project/project/
/project/target/
/target/

### OSX ###
.DS_Store
.AppleDouble
.LSOverride

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
11 changes: 11 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
160 changes: 160 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# OTP(One time password) Authentication Library
This is one time password authentication library for Scala. It has implementation of TOTP ([RFC6238](https://tools.ietf.org/html/rfc6238)) and HOTP ([RFC4226](https://tools.ietf.org/html/rfc4226)), supports to generate pin code as a token and validates the pin by user's secret.

**If you find any differences from RFC specifications, wierd behaviour, bugs or security vulnerability please report or issue me :) asnd I'm always welcome your pull request for implements!**

## Secret Key and Hashing Algorithms
#### Secret Key
Prepare an OTP secret key (`OTPSecretKey`) for the user:
```scala
import ejisan.scalauth.otp.OTPSecretKey
val secret = OTPSecretKey()
val secretFromHex = OTPSecretKey.fromHex("44360b53fc2cc239d74a")
val secretFromBase32 = OTPSecretKey.fromBase32("IQ3AWU74FTBDTV2K")
val secretFromBigInt = OTPSecretKey(BigInt("322117861288342841841482"))
```
`OTPSecretKey()` generates secret key with a random generator that is "NativePRNGNonBlocking" as default. You can specify a random generator by giving `scala.util.Random`:
```scala
import ejisan.scalauth.otp.OTPSecretKey
import scala.util.Random
val secret = OTPSecretKey(new Random(java.security.SecureRandom.getInstance("NativePRNGBlocking")))
```
Encoding to Base63 or Hex string representation:

```scala
import ejisan.scalauth.otp.OTPSecretKey
val secret = OTPSecretKey()
secret.toBase32
secret.toHex
```
#### Hashing Algorithms
Create a TOTP instance with an algorithm, a length of PIN code digits and period of seconds. It supports .
```scala
import ejisan.scalauth.otp.{ OTPHashAlgorithm, TOTP }
val totp = TOTP(OTPHashAlgorithm.SHA1, 6, 30)
```
Supported algorithms (Old versioned Google Authenticator ignores this algorithm):
- `OTPHashAlgorithm.SHA1` (Default)
- `OTPHashAlgorithm.SHA256`
- `OTPHashAlgorithm.SHA512`

## Usage of TOTP
#### PIN Code Generation
Generate PIN code as toke:
```scala
import ejisan.scalauth.otp.{ OTPSecretKey, OTPHashAlgorithm, TOTP }
val totp = TOTP(OTPHashAlgorithm.SHA1, 6, 30)
val secret = OTPSecretKey()

totp(secret)
```
Generate PIN code with [time-step](https://tools.ietf.org/html/rfc6238#section-5.2) window:
```scala
totp(secret, 5) // Returns PIN codes that are 5 more time-step.
```
#### PIN Code Validation
Validate PIN code:
```scala
totp([pinCode], secret) // Returns boolean
```
Validate a pin code with [time-step](https://tools.ietf.org/html/rfc6238#section-5.2) window:
```scala
totp([pinCode], secret) // Returns boolean
```
### Cheat Sheet
Generate secret key.
```scala
import ejisan.scalauth.otp.OTPSecretKey

val secret = OTPSecretKey()
secret.toBase32
```
Generate PIN code:

```scala
import ejisan.scalauth.otp.{ OTPSecretKey, OTPHashAlgorithm, TOTP }

val secret = OTPSecretKey.fromBase32("FVKZGY3GSHGB6LZN")
val totp = TOTP(OTPHashAlgorithm.SHA1, 6, 30)

totp(secret)
```
Validate the PIN code:

```scala
val secret = OTPSecretKey.fromBase32("FVKZGY3GSHGB6LZN")
val totp = TOTP(OTPHashAlgorithm.SHA1, 6, 30)

if(totp.validate([pinCode], secret)) {
// User is authenticated
} else {
// User isn't authenticated
}

```

## Usage of HOTP
You need to prepare an OTP secret key (`OTPSecretKey`) for user by [the same way as TOTP](#secret).
#### PIN Code Generation
Generate PIN code as toke with counter `1`:
```scala
import ejisan.scalauth.otp.{ OTPHashAlgorithm, HOTP }
val hotp = HOTP(OTPHashAlgorithm.SHA1, 6)
val secret = OTPSecretKey()
val counter = 1

hotp(secret, counter)
```
Generate PIN code with [look-ahead](https://tools.ietf.org/html/rfc4226#section-7.4) window:
```scala
hotp(secret, counter, 5) // Returns 5 tuples that are (counter, PIN code).
```
#### PIN Code Validation
Validate PIN code:
```scala
hotp([pinCode], secret, counter) // Returns boolean
```
Validate a pin code with [time-step](https://tools.ietf.org/html/rfc6238#section-5.2) window:
```scala
val windowSize = 5
hotp([pinCode], secret, counter, windowSize) // Returns boolean
```
### Cheat Sheet
Generate secret key.
```scala
import ejisan.scalauth.otp.OTPSecretKey

val secret = OTPSecretKey()
secret.toBase32
```
Generate PIN code:

```scala
import ejisan.scalauth.otp.{ OTPSecretKey, OTPHashAlgorithm, HOTP }

val secret = OTPSecretKey.fromBase32("FVKZGY3GSHGB6LZN")
val hotp = HOTP(OTPHashAlgorithm.SHA1, 6)
val counter = 1

hotp(secret, counter)
```
Validate the PIN code:

```scala
val secret = OTPSecretKey.fromBase32("FVKZGY3GSHGB6LZN")
val hotp = HOTP(OTPHashAlgorithm.SHA1, 6)
val counter = 1

if(hotp.validate([pinCode], secret, counter)) {
// User is authenticated
} else {
// User isn't authenticated
}

```



## License
scalauth-otp is licensed under the [Apache License, Version 2.0](./LICENSE).

15 changes: 15 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name := """scalauth-otp"""

organization := "com.ejisan"

version := "1.0.0-SNAPSHOT"

scalaVersion := "2.11.8"

scalacOptions ++= Seq("-feature")

scalacOptions in Test ++= Seq("-Yrangepos")

crossScalaVersions := Seq("2.10.6", scalaVersion.value)

libraryDependencies += "org.specs2" %% "specs2-core" % "3.8.3" % "test"
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.11
17 changes: 17 additions & 0 deletions src/main/scala/ejisan/scalauth/otp/Base32.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ejisan.scalauth.otp

import scala.math.BigInt

/** Base32 encoder and decoder */
private[otp] object Base32 {
private val alphabet: String
= new String((('A' to 'Z') ++ ('2' to '7')).toArray)
/** Encodes BigInt to base32 encoded string */

def encode(value: BigInt): String
= new String(value.toString(32).toCharArray.map(_.asDigit).map(alphabet(_)))

/** Decodes base32 encoded string to BigInt */
def decode(value: String): BigInt
= BigInt(value.toCharArray.map(alphabet.indexOf(_)).map(BigInt(_)).map(_.toString(32)).mkString, 32)
}
50 changes: 50 additions & 0 deletions src/main/scala/ejisan/scalauth/otp/GoogleAuthenticator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ejisan.scalauth.otp

/** Default instances for Google Authenticator.
*
* {{{
* val secret: OTPSecretKey = OTPSecretKey()
* val counterForHOTP: Int = 1
*
* // PIN Code generation
* val pin1: String = GoogleAuthenticator.hotp(secret, counterForHOTP)
* val pin2: String = GoogleAuthenticator.totp(secret)
*
* // PIN Code validation
* GoogleAuthenticator.hotp.validate(pin1, secret, counterForHOTP)
* GoogleAuthenticator.totp.validate(pin2, secret)
* }}}
*/
object GoogleAuthenticator {
/** Creates a HOTP utility with a given algorithm and digits.
*
* @param algorithm the name of selected hashing algorithm
* @param digits the length of returning OTP pin code string
*/
def hotp(algorithm: OTPHashAlgorithm = OTPHashAlgorithm.SHA1, digits: Int = 6): HOTP = {
require(digits >= 6, s"`digits` must be greater than or equal to 6, but it is '$digits'.")
HOTP(algorithm, digits)
}

/** Default HOTP instance for Google Authenticator.
* It configured with digits: 6.
*/
val hotp: HOTP = hotp()

/** Creates a TOTP utility with a given algorithm, digits and period.
*
* @param algorithm the name of selected hashing algorithm
* @param digits the length of returning OTP pin code string
* @param period the period of seconds
*/
def totp(algorithm: OTPHashAlgorithm = OTPHashAlgorithm.SHA1, digits: Int = 6, period: Int = 30): TOTP = {
require(digits >= 6, s"`digits` must be greater than or equal to 6, but it is '$digits'.")
require(period >= 5, s"`period` must be greater than or equal to 5, but it is '$period'.")
TOTP(algorithm, digits, period)
}

/** Default TOTP instance for Google Authenticator.
* It configured with digits: 6 and period 30.
*/
val totp: TOTP = totp()
}
103 changes: 103 additions & 0 deletions src/main/scala/ejisan/scalauth/otp/HOTP.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package ejisan.scalauth.otp

/** A HOTP(HMAC-based One-time Password Algorithm).
*
* {{{
* import ejisan.scalauth.otp._
*
* val secret: OTPSecretKey = OTPSecretKey()
* val hotp: HOTP = HOTP(OTPHashAlgorithm.SHA1)
* val counter: Int = 1
*
* // Pin code generation
* hotp(secret, counter) // : String
* hotp(secret, counter, 5) // : Seq[String]
*
* // Pin code validation
* hotp.validate(pin, secret, counter) // : Boolean
* hotp.validate(pin, secret, counter, 5) // : Boolean (Look-ahead 5 more count)
* }}}
*
* @param algorithm the name of selected hashing algorithm
* @param digits the length of returning OTP pin code string
*/
class HOTP private (val algorithm: OTPHashAlgorithm, val digits: Int) {
/** Generates OTP pin code with a given user's secret and a counter.
*
* @param secret the user's secret
* @param counter the counter number
*
* @return pin code digits
*/
def apply(secret: OTPSecretKey, counter: Long): String
= HOTP(algorithm, digits, secret, counter)

/** Generates OTP pin codes with a given user's secret, a counter and a window size.
*
* @param secret the user's secret
* @param counter the counter number
* @param window the look-ahead window size
*
* @return a sequence of tuple typed (Long, String) as (counter, pin code digits)
*/
def apply(secret: OTPSecretKey, counter: Long, window: Int): Seq[(Long, String)]
= (counter until counter + window).map(c => (c, HOTP(algorithm, digits, secret, c)))

/** Validates a given OTP pin code with a given user's secret and a counter.
*
* @param pin the pin code that user generated
* @param secret the user's secret
* @param counter the counter number
*/
def validate(pin: String, secret: OTPSecretKey, counter: Long): Boolean
= pin == apply(secret, counter)

/** Validates a given OTP pin code with a given user's secret, a counter and a window size.
*
* @param pin the pin code that user generated
* @param secret the user's secret
* @param counter the counter number
* @param window the look-ahead window size
*
* @return an optional matched counter number
*/
def validate(pin: String, secret: OTPSecretKey, counter: Long, window: Int): Option[Long]
= apply(secret, counter, window).find(_._2 == pin).map(_._1)
}

/** Factory for [[ejisan.scalauth.otp.HOTP]] instances
* and an implementation of HOTP pin code generation.
**/
object HOTP {
/** An implementation of HOTP pin code generation.
*
* @param algorithm the name of selected hashing algorithm
* @param digits the length of returning OTP pin code string
* @param secret the user's secret
* @param counter the counter number
*
* @return pin code digits
*/
def apply(algorithm: OTPHashAlgorithm, digits: Int, secret: OTPSecretKey, counter: Long): String = {
val msg = BigInt(counter).toByteArray.reverse.padTo(8, 0.toByte).reverse
val hash = OTPHasher(algorithm, secret, msg)
val offset = hash(hash.length - 1) & 0xf
val binary = ((hash(offset) & 0x7f) << 24) |
((hash(offset + 1) & 0xff) << 16) |
((hash(offset + 2) & 0xff) << 8 |
(hash(offset + 3) & 0xff))
val otp = binary % (scala.math.pow(10, digits)).toLong
("0" * digits + otp.toString).takeRight(digits)
}

/** Creates a HOTP with a given algorithm and digits.
*
* @param algorithm the name of selected hashing algorithm
* @param digits the length of returning OTP pin code string
*/
def apply(algorithm: OTPHashAlgorithm, digits: Int): HOTP = {
// Requirements
require(digits > 0, s"digits must be greater than 0, but it is ($digits)")
new HOTP(algorithm, digits)
}
}
Loading

0 comments on commit ddec86c

Please sign in to comment.