forked from ejisan/kuro-otp
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ddec86c
Showing
16 changed files
with
705 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
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 |
This file contains 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
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. |
This file contains 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
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). | ||
|
This file contains 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
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" |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sbt.version=0.13.11 |
This file contains 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
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
50
src/main/scala/ejisan/scalauth/otp/GoogleAuthenticator.scala
This file contains 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
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() | ||
} |
This file contains 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
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) | ||
} | ||
} |
Oops, something went wrong.