Skip to content

Commit

Permalink
Basic implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
philnash committed Jan 7, 2017
1 parent 3da6994 commit fa3fe7b
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 15 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2016 Phil Nash
Copyright (c) 2017 Phil Nash

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# crotp
# CrOTP

TODO: Write a description here
The Crystal One Time Password library.

## Installation

Expand All @@ -14,15 +14,46 @@ dependencies:
## Usage
### HOTP
```crystal
require "crotp"

hotp = CrOTP::HOTP.new("secret")

# Generate a code
hotp.generate(counter)
# => "423748"

# Verify code
hotp.verify(token, counter)
# => true
```

TODO: Write usage instructions here
### TOTP

```crystal
require "crotp"
totp = CrOTP::TOTP.new("secret")
# Generate a code
totp.generate
# => "423748"
## Development
# Generate a code at a specific time stamp
totp.generate(at: 3.minutes.ago)
# => "923832"
TODO: Write development instructions here
# Verify code (verifies against the current time)
totp.verify(token)
# => true
# Verify code at a specific time stamp
totp.verify(token, at: 3.minutes.ago)
# => true
```

## Contributing

Expand Down
40 changes: 40 additions & 0 deletions spec/crotp/hotp_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require "./../spec_helper"

describe CrOTP::HOTP do
# Using data from Appendix D of RFC 4226
# https://tools.ietf.org/html/rfc4226#page-32
results = [
"755224",
"287082",
"359152",
"969429",
"338314",
"254676",
"287922",
"162583",
"399871",
"520489"
]
secret = "12345678901234567890"
hotp = CrOTP::HOTP.new(secret)

describe "generate a token" do
results.each_with_index do |result, counter|
it "matches the RFC example for #{counter} counter" do
hotp.generate(counter).should eq(result)
end
end
end

describe "verify the token" do
results.each_with_index do |result, counter|
it "verifies the RFC example for the result at #{counter}" do
hotp.verify(result, counter).should be_true
end

it "does not verify the RFC example for the result with counter #{counter} + 1" do
hotp.verify(result, counter + 1).should be_false
end
end
end
end
49 changes: 49 additions & 0 deletions spec/crotp/totp_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require "./../spec_helper"

describe CrOTP::TOTP do
# Using data from Appendix B of RFC 6238
# https://tools.ietf.org/html/rfc6238#appendix-B
results = [
{ 59, "94287082" },
{ 1111111109, "07081804" },
{ 1111111111, "14050471" },
{ 1234567890, "89005924" },
{ 2000000000, "69279037" },
{ 20000000000, "65353130" },
]
secret = "12345678901234567890"

describe "generate a token" do
results.each do |(time, result)|
it "matches the RFC example at #{time}" do
totp = CrOTP::TOTP.new(secret)
totp.generate(at: time).should eq(result[2, 6])
end
end

results.each do |(time, result)|
it "matches the RFC example at #{time} for 8 digits" do
totp = CrOTP::TOTP.new(secret, digits = 8)
totp.generate(at: time).should eq(result)
end
end
end

describe "verify a token" do
totp = CrOTP::TOTP.new(secret)

it "verifies the current time" do
totp.verify(totp.generate(), at: Time.now)
end

results.each do |(time,result)|
it "verifies the RFC example for the result at time #{time}" do
totp.verify(result[2, 6], at: time).should be_true
end

it "does not verify the RFC example for the result with time #{time} + 1 minute" do
totp.verify(result[2, 6], at: time + 60).should be_false
end
end
end
end
7 changes: 1 addition & 6 deletions spec/crotp_spec.cr
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
require "./spec_helper"

describe Crotp do
# TODO: Write tests

it "works" do
false.should eq(true)
end
describe CrOTP do
end
8 changes: 5 additions & 3 deletions src/crotp.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require "./crotp/*"
require "./crotp/otp"
require "./crotp/hotp"
require "./crotp/totp"
require "./crotp/version"

module Crotp
# TODO Put your code here
module CrOTP
end
16 changes: 16 additions & 0 deletions src/crotp/hotp.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module CrOTP
class HOTP
include CrOTP::OTP

def initialize(@secret : String, @digits : Int = 6)
end

def generate(counter : Int) : String
generate_otp(counter)
end

def verify(token : String, counter : Int) : Bool
verify_otp(token, counter)
end
end
end
74 changes: 74 additions & 0 deletions src/crotp/otp.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require "openssl/hmac"
require "crypto/subtle"

module CrOTP
module OTP
private def generate_otp(counter : Int) : String
digest = OpenSSL::HMAC.digest(:sha1, @secret, int_to_bytes(counter))
truncated = truncate(digest)
(truncated % 10**@digits).to_s.rjust(@digits, '0')
end

private def truncate(hmac : Bytes)
offset = (hmac[-1] & 0xf)
code = (hmac[offset].to_i & 0x7f) << 24 |
(hmac[offset + 1].to_i & 0xff) << 16 |
(hmac[offset + 2].to_i & 0xff) << 8 |
(hmac[offset + 3].to_i & 0xff)
end

private def verify_otp(token : String, counter : Int)
otp = generate_otp(counter)
Crypto::Subtle.constant_time_compare(otp, token)
end

# Implementation inspired by this conversation: https://groups.google.com/forum/#!topic/crystal-lang/KbdUXgoFYR4.
# Probably best to rewrite in a version that will produce the same order
# regardless of the underlying operating system.
private def int_to_bytes(integer : Int64)
pointer = pointerof(integer).as({UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8}*)
tuple = pointer.value
Slice.new(8) { |i| tuple[i] }.reverse!
end

private def int_to_bytes(integer : Int32)
pointer = pointerof(integer).as({UInt8, UInt8, UInt8, UInt8}*)
tuple = pointer.value
Slice.new(8) { |i| tuple[i]? || 0_u8 }.reverse!
end

private def int_to_bytes(integer : Int16)
pointer = pointerof(integer).as({UInt8, UInt8}*)
tuple = pointer.value
Slice.new(8) { |i| tuple[i]? || 0_u8 }.reverse!
end

private def int_to_bytes(integer : Int8)
pointer = pointerof(integer).as({UInt8}*)
tuple = pointer.value
Slice.new(8) { |i| tuple[i]? || 0_u8 }.reverse!
end

private def int_to_bytes(integer : UInt64)
pointer = pointerof(integer).as({UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8}*)
tuple = pointer.value
Slice.new(8) { |i| tuple[i]? || 0_u8 }.reverse!
end

private def int_to_bytes(integer : UInt32)
pointer = pointerof(integer).as({UInt8, UInt8, UInt8, UInt8}*)
tuple = pointer.value
Slice.new(8) { |i| tuple[i]? || 0_u8 }.reverse!
end

private def int_to_bytes(integer : UInt16)
pointer = pointerof(integer).as({UInt8, UInt8}*)
tuple = pointer.value
Slice.new(8) { |i| tuple[i]? || 0_u8 }.reverse!
end

private def int_to_bytes(integer : UInt8)
Slice.new(8) { |i| tuple[i]? || 0_u8 }.reverse!
end
end
end
26 changes: 26 additions & 0 deletions src/crotp/totp.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module CrOTP
class TOTP
include CrOTP::OTP

def initialize(@secret : String, @digits : Int = 6)
end

def generate(at : Int = Time.now.epoch) : String
counter = at / 30
generate_otp(counter)
end

def generate(at : Time) : String
generate(at.epoch)
end

def verify(token : String, at : Int = Time.now.epoch) : Bool
counter = at / 30
verify_otp(token, counter)
end

def verify(token : String, at : Time) : Bool
verify(token, at.epoch)
end
end
end

0 comments on commit fa3fe7b

Please sign in to comment.