secure_equals 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +27 -0
- data/lib/secure_equals.rb +23 -0
- data/secure_equals.gemspec +12 -0
- data/test/timing_attack.rb +80 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: f9883242630ffc2552eb1859d0a0b869f218237b
|
|
4
|
+
data.tar.gz: 0222f297a7683a1927b92241e7fde4de2c9618f4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0ba9b81d792b35adcabbe6163debddf1f8107b11c62b5c12d60cba6d4feab8577ae08a15aaea9bfe692712a5fce9d1d8ad84ecb5c2888a616646381ffdad3700
|
|
7
|
+
data.tar.gz: e66e06d162ac9d9983533330ea9d9e77d873bbbc4f9e63144dedfd5d196eaf02720a3c7dce13dceacd78618a8a48424731caac22959995ede2ccc63ffc60c9e7
|
data/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Secure equals provides a constant time equality method for ruby strings.
|
|
2
|
+
|
|
3
|
+
By avoiding any branches that depend on the similarity of the two strings,
|
|
4
|
+
we make it significantly harder for an attacker to discover properties of
|
|
5
|
+
the secret by observing the time it takes to run the comparison.
|
|
6
|
+
|
|
7
|
+
You should use this for checking checksums or shared secrets in your code.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
You can either `gem install secure_equals` or add `gem 'secure_equals'` to your Gemfile and then run `bundle install`
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
SecureEquals.equal? checksum, @params[:checksum]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## TODO
|
|
20
|
+
|
|
21
|
+
I was unable to implement a successful timing attack against ruby's string == on my Mac Book Pro; so while the code here looks promising, I've not actually been able to verify that it actually solves the problem.
|
|
22
|
+
|
|
23
|
+
We should try running the attack on 16- and 32- byte sized chunks instead of the character-by-character mode, as I suspect the problem is memcmp(). Alternatively there might be a way to get higher resolution timings, or to use some stats that I don't know about, to make it work.
|
|
24
|
+
|
|
25
|
+
## BUGS
|
|
26
|
+
|
|
27
|
+
I'm pretty sure this must already exist somewhere...
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SecureEquals
|
|
2
|
+
# Provides an equality method on strings that is not vulnerable to timing attacks.
|
|
3
|
+
#
|
|
4
|
+
# This prevents attackers from being able to guess the answer byte-by-byte using
|
|
5
|
+
# tiny differences in response time.
|
|
6
|
+
#
|
|
7
|
+
# @param [String] The secret string
|
|
8
|
+
# @param [String] The string provided by the user
|
|
9
|
+
# @return [Boolean] Are the strings the same?
|
|
10
|
+
#
|
|
11
|
+
def self.equal?(mine, theirs)
|
|
12
|
+
mine = mine.to_str
|
|
13
|
+
theirs = theirs.to_str
|
|
14
|
+
return false unless mine.length == theirs.length
|
|
15
|
+
|
|
16
|
+
difference = 0
|
|
17
|
+
0.upto(mine.length - 1) do |i|
|
|
18
|
+
difference |= (mine[i].ord ^ theirs[i].ord)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
difference == 0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = "secure_equals"
|
|
3
|
+
s.version = "0.1"
|
|
4
|
+
s.platform = Gem::Platform::RUBY
|
|
5
|
+
s.author = "Conrad Irwin"
|
|
6
|
+
s.email = "conrad.irwin@gmail.com"
|
|
7
|
+
s.homepage = "http://github.com/Conradirwin/secure_equals"
|
|
8
|
+
s.summary = "Provides constant time equality for ruby strings"
|
|
9
|
+
s.description = "Constant time equality (also known as time insensitive equality) lets you compare user-provided strings with secrets in a way that does not leak data about those secrets."
|
|
10
|
+
s.files = `git ls-files`.split("\n")
|
|
11
|
+
s.require_path = "lib"
|
|
12
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require_relative '../lib/secure_equals'
|
|
2
|
+
require 'securerandom'
|
|
3
|
+
require 'hitimes'
|
|
4
|
+
GC.disable
|
|
5
|
+
|
|
6
|
+
class Box
|
|
7
|
+
def initialize
|
|
8
|
+
@secret = '0123456789abcdeffedcba0987654321'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# The expected score of a random guess is 0.5.
|
|
12
|
+
# The expected score of the correct answer is 1.
|
|
13
|
+
#
|
|
14
|
+
# If you are able to get scores consistently higher
|
|
15
|
+
# than 0.5 using only the guess method, then you
|
|
16
|
+
# have a successful timing attack.
|
|
17
|
+
def score(str)
|
|
18
|
+
score = 0.0
|
|
19
|
+
mine = @secret.each_byte.map{ |x| x.to_s(2) }.join
|
|
20
|
+
theirs = str.each_byte.map{ |x| x.to_s(2) }.join
|
|
21
|
+
|
|
22
|
+
mine.each_byte.zip(theirs.each_byte) do |a,b|
|
|
23
|
+
score += 1 if a == b
|
|
24
|
+
end
|
|
25
|
+
(score / mine.length)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Weak < Box
|
|
29
|
+
def guess(str)
|
|
30
|
+
0.upto(@secret.length - 1) do |i|
|
|
31
|
+
return false unless @secret[i] == str[i]
|
|
32
|
+
end
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Standard < Box
|
|
38
|
+
def guess(str)
|
|
39
|
+
@secret == str
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Secure < Box
|
|
44
|
+
def guess(str)
|
|
45
|
+
SecureEquals.same? @secret, str
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def brute_force(box, trials)
|
|
51
|
+
scores = []
|
|
52
|
+
1.times do
|
|
53
|
+
guess = '0' * 32
|
|
54
|
+
(0..32).each do |pos|
|
|
55
|
+
max = 0
|
|
56
|
+
result = nil
|
|
57
|
+
this_time = guess.dup
|
|
58
|
+
'abcdef0123456789'.each_char do |letter|
|
|
59
|
+
this_time[pos] = letter
|
|
60
|
+
time = Hitimes::Interval.measure do
|
|
61
|
+
trials.times{ box.guess this_time }
|
|
62
|
+
end
|
|
63
|
+
if time > max
|
|
64
|
+
max = time
|
|
65
|
+
result = letter
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
guess[pos] = result
|
|
69
|
+
end
|
|
70
|
+
scores << box.score(guess)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
puts "average: #{scores.inject(&:+) / scores.size}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
10.times do
|
|
77
|
+
brute_force Box::Weak.new, 1000
|
|
78
|
+
brute_force Box::Standard.new, 100000
|
|
79
|
+
brute_force Box::Secure.new, 1000000
|
|
80
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: secure_equals
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: '0.1'
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Conrad Irwin
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2013-11-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Constant time equality (also known as time insensitive equality) lets
|
|
14
|
+
you compare user-provided strings with secrets in a way that does not leak data
|
|
15
|
+
about those secrets.
|
|
16
|
+
email: conrad.irwin@gmail.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/secure_equals.rb
|
|
23
|
+
- secure_equals.gemspec
|
|
24
|
+
- test/timing_attack.rb
|
|
25
|
+
homepage: http://github.com/Conradirwin/secure_equals
|
|
26
|
+
licenses: []
|
|
27
|
+
metadata: {}
|
|
28
|
+
post_install_message:
|
|
29
|
+
rdoc_options: []
|
|
30
|
+
require_paths:
|
|
31
|
+
- lib
|
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
33
|
+
requirements:
|
|
34
|
+
- - '>='
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '0'
|
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - '>='
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
requirements: []
|
|
43
|
+
rubyforge_project:
|
|
44
|
+
rubygems_version: 2.0.3
|
|
45
|
+
signing_key:
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Provides constant time equality for ruby strings
|
|
48
|
+
test_files: []
|
|
49
|
+
has_rdoc:
|