snowden 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ tags
19
+ bundle
20
+ bin
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in snowden.gemspec
4
+ gemspec
5
+
6
+ gem "rspec-expectations", :github => "rspec/rspec-expectations"
7
+ gem "rspec-mocks", :github => "rspec/rspec-mocks"
8
+ gem "rspec-core", :github => "rspec/rspec-core"
9
+ gem "redis"
10
+ gem "hiredis"
11
+ gem "redis_gun"
12
+ gem "simplecov"
13
+ gem "yard"
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Cambridge Healthcare Ltd.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,151 @@
1
+ # Snowden
2
+
3
+ Snowden is a gem for managing encrypted search indices. It can do fuzzy search
4
+ on text indices and supports pluggable backends.
5
+
6
+ **Snowden currently sits at version `0.9.0`, we want some feedback before
7
+ making the API concrete. That said, we're pretty happy with this and using it
8
+ in production. Please send issues/pull requests if you have problems.**
9
+
10
+ The basic idea behind Snowden is captured in
11
+ [this paper](http://www.cs.cityu.edu.hk/~congwang/papers/INFOCOM10-search.pdf).
12
+
13
+ The search algorithm works by encrypting "wildcard strings" over the key in
14
+ the index that you're trying to encrypt. When you search you construct a wildcard
15
+ set over your search term. You encrypt the search wildcard set, and this
16
+ will produce a matching encrypted value in the stored wildcard set if any
17
+ of the wildcards overlap.
18
+
19
+ An example of this can be seen below:
20
+
21
+ ```
22
+ Store: "bacon"
23
+
24
+ Wildcard set (size 1):
25
+
26
+ ["bacon", "*bacon", "b*acon", "ba*con", "bac*on", "baco*n", "bacon*", "*acon", "b*con", "ba*on", "bac*n", "baco*"]
27
+
28
+ Search: "baco":
29
+
30
+ Wildcard set (size 1):
31
+
32
+ ["baco", "*baco", "b*aco", "ba*co", "bac*o", "baco*", "*aco", "b*co", "ba*o", "bac*"]
33
+
34
+ Matches:
35
+
36
+ ["baco*"]
37
+ ```
38
+
39
+ The encryption we use for keys encrypts the same string as the same value
40
+ so this match can happen without the values being decrypted.
41
+
42
+
43
+ ## Installation
44
+
45
+ Add this line to your application's Gemfile:
46
+
47
+ gem 'snowden'
48
+
49
+ And then execute:
50
+
51
+ $ bundle
52
+
53
+ Or install it yourself as:
54
+
55
+ $ gem install snowden
56
+
57
+ ## Usage
58
+
59
+ ```ruby
60
+ require 'snowden'
61
+
62
+ # 256 bit aes with 128 bit block
63
+ aes_key = "a"*(256/8)
64
+ aes_iv = "b"*(128/8)
65
+
66
+ index = Snowden.new_encrypted_index(aes_key, aes_iv, Snowden::Backends::HashBackend.new)
67
+ searcher = Snowden.new_encrypted_searcher(aes_key, aes_iv, index)
68
+
69
+ index.store("bacon", "bits")
70
+
71
+ searcher.search("bac")
72
+ # => ["bits"]
73
+ ```
74
+
75
+ ## Backends and namespacing
76
+
77
+ Snowden supports multiple backends for storing your encrypted search indices,
78
+ two backends are provided as part of the gem:
79
+
80
+ * An in memory hash backend `Snowden::Backends::HashBackend`
81
+ * A redis backend `Snowden::Backends::RedisBackend`
82
+
83
+ Both support taking a namespace, which allows you to store multiple different
84
+ encrypted indices in the same store. The redis backend also takes a
85
+ `Redis` object from the [redis](https://github.com/redis/redis-rb) gem to serve
86
+ as its connection to the redis server.
87
+
88
+ An example of the use of the redis backend is:
89
+
90
+ ```ruby
91
+ require "redis"
92
+
93
+ redis = Redis.new(:driver => :hiredis)
94
+ redis_backend = Snowden::Backends::RedisBackend.new("index_namespace", redis)
95
+
96
+ aes_key = OpenSSL::Random.random_bytes(256/8)
97
+ aes_iv = OpenSSL::Random.random_bytes(128/8)
98
+
99
+ index = Snowden.new_encrypted_index(aes_key, aes_iv, redis_backend)
100
+ #...
101
+ ```
102
+
103
+
104
+ ## Configuration
105
+
106
+ Snowden has a core configuration object that allows you to change various
107
+ aspects of the gem's operation.
108
+
109
+ ###Changing the cipher used by Snowden
110
+
111
+ ```ruby
112
+ Snowden.configuration.cipher_spec = "RC4"
113
+
114
+ #Sometime later:
115
+ index = Snowden.new_encrypted_index(key, iv, Snowden::Backends::HashBackend.new)
116
+ ```
117
+
118
+ For a complete list of possible ciphers you can use this snippet in `irb`
119
+
120
+ ```ruby
121
+ OpenSSL::Cipher.ciphers.each do |c| p c end; nil
122
+ ```
123
+
124
+ The default cipher in Snowden is `AES-256-CBC` which we believe to be secure
125
+ enough for our purposes, your mileage may vary.
126
+
127
+ 32 bytes of random padding are added to the front of ciphertexts in Snowden to
128
+ prevent the same value stored under many different index keys being
129
+ diffentiable when encrypted under the same key and IV.
130
+
131
+ ##Implementing your own backends
132
+
133
+ A Snowden backend is a ruby class that:
134
+
135
+ * Can be constructed with a namespace
136
+ * Responds to `#save(key, value)` which returns nil
137
+ * Responds to `#find(key)` which returns all the values saved under that key
138
+
139
+ The two backends built into Snowden (in `lib/snowden/backends`) serve as
140
+ reference implementations of Snowden backends.
141
+
142
+ ## Contributing
143
+
144
+ Please note: you need to have a redis server running on the default port to
145
+ run the specs, this is for integration testing the `RedisBackend` class.
146
+
147
+ 1. Fork it
148
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
149
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
150
+ 4. Push to the branch (`git push origin my-new-feature`)
151
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,76 @@
1
+ require "snowden/backends/hash_backend"
2
+ require "snowden/backends/redis_backend"
3
+ require "snowden/configuration"
4
+ require "snowden/crypto"
5
+ require "snowden/wildcard_generator"
6
+ require "snowden/encrypted_search_index"
7
+ require "snowden/encrypted_searcher"
8
+
9
+ module Snowden
10
+ # A handle to the Snowden configuration object used elsewhere in the gem
11
+ #
12
+ # @return [Snowden::Configuration] the configuration object
13
+ def self.configuration
14
+ @configuration ||= Snowden::Configuration.new
15
+ end
16
+
17
+ # Creates a new index that will encrypt keys and values stored within it
18
+ #
19
+ # @param key [String]
20
+ # a bytestring key for the underlying encryption algorithm.
21
+ #
22
+ # @param iv [String]
23
+ # a bytestring iv for the underlying encryption algorithm.
24
+ #
25
+ # @param backend [Snowden::Backend]
26
+ # an object that implements the snowden backend protocol.
27
+ #
28
+ # @return [Snowden::EncryptedSearchIndex]
29
+ # a snowden index to store values in.
30
+ #
31
+ def self.new_encrypted_index(key, iv, backend)
32
+ EncryptedSearchIndex.new(
33
+ :crypto => crypto_for(key, iv),
34
+ :backend => backend,
35
+ :wildcard_generator => wildcard_generator,
36
+ )
37
+ end
38
+
39
+ # Creates a new searcher for a snowden index
40
+ #
41
+ # @param key [String]
42
+ # a bytestring key for the underlying encryption algorithm.
43
+ # Note: the key and iv must match the ones passed to create the index
44
+ #
45
+ # @param iv [String]
46
+ # a bytestring iv for the underlying encryption algorithm.
47
+ # Note: the key and iv must match the ones passed to create the index
48
+ #
49
+ # @param index [Snowden::EncryptedSearchIndex]
50
+ # the index to search.
51
+ #
52
+ # @return [Snowden::EncryptedSearcher]
53
+ # a searcher for the index.
54
+ def self.new_encrypted_searcher(key, iv, index)
55
+ EncryptedSearcher.new(
56
+ :crypto => crypto_for(key, iv),
57
+ :index => index,
58
+ :wildcard_generator => wildcard_generator,
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ def self.wildcard_generator
65
+ WildcardGenerator.new(:edit_distance => configuration.edit_distance)
66
+ end
67
+
68
+ def self.crypto_for(key, iv)
69
+ Crypto.new(
70
+ :key => key,
71
+ :iv => iv,
72
+ :cipher_spec => configuration.cipher_spec,
73
+ :padding_size => configuration.padding_byte_size
74
+ )
75
+ end
76
+ end
@@ -0,0 +1,41 @@
1
+ module Snowden
2
+ module Backends
3
+ #@api private
4
+ SNOWDEN_BACKEND_HASH = {}
5
+
6
+ class HashBackend
7
+ #Creates a new redis backend
8
+ #
9
+ # @param namespace [String] the string this backend is namespaced under.
10
+ # @param hash [Hash] a Hash object instance to save values in.
11
+ def initialize(namespace="", hash=SNOWDEN_BACKEND_HASH)
12
+ @namespace = namespace
13
+ @hash = hash
14
+ end
15
+
16
+ #Saves a value in this index
17
+ #
18
+ # @param key [String] the string key to save the value under.
19
+ # @param value [String] the value to save.
20
+ def save(key, value)
21
+ @hash[namespaced_key(key)] ||= []
22
+ @hash[namespaced_key(key)] << value
23
+ nil
24
+ end
25
+
26
+ #Finds a value in this index
27
+ #
28
+ # @param key [String] the string key to search the index for.
29
+ # @return [ [String] ] a list of strings that matched the namespaced key.
30
+ def find(key)
31
+ @hash.fetch(namespaced_key(key), [])
32
+ end
33
+
34
+ private
35
+
36
+ def namespaced_key(key)
37
+ [@namespace, key]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,42 @@
1
+ require "redis"
2
+
3
+ module Snowden
4
+ module Backends
5
+ class RedisBackend
6
+ #Creates a new redis backend
7
+ #
8
+ # @param namespace [String] the string this backend is namespaced under.
9
+ # @param redis [Redis] a Redis object instance to talk to a redis
10
+ # database.
11
+ def initialize(namespace="", redis=Redis.new(:driver => :hiredis))
12
+ @namespace = namespace
13
+ @redis = redis
14
+ end
15
+
16
+ #Saves a value in this index
17
+ #
18
+ # @param key [String] the string key to save the value under.
19
+ # @param value [String] the value to save.
20
+ def save(key, value)
21
+ redis.lpush(namespaced_key(key), value)
22
+ nil
23
+ end
24
+
25
+ #Finds a value in this index
26
+ #
27
+ # @param key [String] the string key to search the index for.
28
+ # @return [ [String] ] a list of strings that matched the namespaced key.
29
+ def find(key)
30
+ redis.lrange(namespaced_key(key), 0, -1)
31
+ end
32
+
33
+ private
34
+
35
+ def namespaced_key(key)
36
+ namespace + ":" + key
37
+ end
38
+
39
+ attr_reader :redis, :namespace
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,28 @@
1
+ module Snowden
2
+ #The object that holds all the configuration details for Snowden
3
+ #@attr edit_distance [Integer] the size of the edit distance sets that
4
+ # are created when searching and
5
+ # storing strings.
6
+ # See an example at:
7
+ # https://gist.github.com/samphippen/6621771.
8
+ # Defaults to 3.
9
+ #
10
+ #@attr cipher_spec [String] an OpenSSL cipher spec to use with Snowden.
11
+ # Defaults to "AES-256-CBC".
12
+ #
13
+ #@attr padding_byte_size [Integer] the amount of random padding to add to
14
+ # values stored in the index. Defaults to 32.
15
+ # Change at your own risk.
16
+ # Never set to lower than 2 blocks if you're
17
+ # using a block cipher.
18
+ class Configuration
19
+ attr_accessor :edit_distance, :cipher_spec, :padding_byte_size, :backend
20
+
21
+ #Sets up the configuration object
22
+ def initialize
23
+ @edit_distance = 3
24
+ @cipher_spec = "AES-256-CBC"
25
+ @padding_byte_size = 32
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ require "openssl"
2
+
3
+ module Snowden
4
+ #@api private
5
+ class Crypto
6
+ def initialize(args)
7
+ @key = args.fetch(:key)
8
+ @iv = args.fetch(:iv)
9
+ @cipher_spec = args.fetch(:cipher_spec)
10
+ @padding_size = args.fetch(:padding_size)
11
+ end
12
+
13
+ def decrypt(data)
14
+ cipher(:decrypt, data)
15
+ end
16
+
17
+ def encrypt(data)
18
+ cipher(:encrypt, data)
19
+ end
20
+
21
+ def padded_encrypt(data)
22
+ encrypt(OpenSSL::Random.random_bytes(padding_size) + data)
23
+ end
24
+
25
+ def padded_decrypt(data)
26
+ decrypt(data)[padding_size..-1]
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :key, :iv, :cipher_spec, :padding_size
32
+
33
+ def cipher(mode, data)
34
+ c = symmetric_cipher
35
+ c.public_send(mode)
36
+ c.key = key
37
+ c.iv = iv
38
+
39
+ c.update(data) + c.final
40
+ end
41
+
42
+ def symmetric_cipher
43
+ OpenSSL::Cipher::Cipher.new(cipher_spec)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,53 @@
1
+ module Snowden
2
+ class EncryptedSearchIndex
3
+ #Creates a new search index
4
+ #
5
+ # @param args [Hash]
6
+ # A hash that must contain the following keys:
7
+ # * :crypto - an instance of Snowden::Crypto primed with some key and
8
+ # iv
9
+ # * :backend - a Snowden::Backends compatible backend.
10
+ # * :wildcard_generator - an instance of Snowden::WildcardGenerator
11
+ def initialize(args)
12
+ @crypto = args.fetch(:crypto)
13
+ @backend = args.fetch(:backend)
14
+ @wildcard_generator = args.fetch(:wildcard_generator)
15
+ end
16
+
17
+ #Stores a value under the key
18
+ #
19
+ # @param key [String] the key to store the value under
20
+ # @param value [String] the value to store in the key
21
+ #
22
+ # @return nil
23
+ def store(key, value)
24
+ wildcard_generator.wildcards(key).each do |wildcard|
25
+ backend.save(encrypt_key(wildcard), encrypt_value(value))
26
+ end
27
+ nil
28
+ end
29
+
30
+ # Looks up the key in the backend
31
+ #
32
+ # @api private
33
+ #
34
+ # @param key [String] the key to look up in the backend. Note: the key does
35
+ # not have wildcarding applied to it. Calling this
36
+ # method directly is probably a bad idea.
37
+ def search(key)
38
+ backend.find(key)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :backend, :crypto, :wildcard_generator, :bytestream_generator
44
+
45
+ def encrypt_key(key)
46
+ crypto.encrypt(key)
47
+ end
48
+
49
+ def encrypt_value(value)
50
+ crypto.padded_encrypt(value)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,40 @@
1
+ module Snowden
2
+ class EncryptedSearcher
3
+ #Creates a new search index
4
+ #
5
+ # @param args [Hash]
6
+ # A hash that must contain the following keys:
7
+ # * :crypto - an instance of Snowden::Crypto primed with some key and
8
+ # iv. Note: the key and iv this crypto uses must match the
9
+ # ones used by the index
10
+ # * :index - a Snowden::EncryptedSearchIndex instance .
11
+ # * :wildcard_generator - an instance of Snowden::WildcardGenerator
12
+ def initialize(args)
13
+ @crypto = args.fetch(:crypto)
14
+ @index = args.fetch(:index)
15
+ @wildcard_generator = args.fetch(:wildcard_generator)
16
+ end
17
+
18
+ # Looks up the search string in the index.
19
+ #
20
+ # @param search_string [String] the string to search the index for
21
+ #
22
+ # @return [ [String] ] a list of strings that were matched by the search
23
+ # string in the index.
24
+ def search(search_string)
25
+ wildcard_generator.wildcards(search_string).flat_map { |wildcard|
26
+ encrypted_values = encrypted_values_for_key(wildcard)
27
+ encrypted_values.map {|v| crypto.padded_decrypt(v) }
28
+ }.uniq
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :wildcard_generator, :crypto, :index, :padding_size
34
+
35
+ def encrypted_values_for_key(key)
36
+ encrypted_key = crypto.encrypt(key)
37
+ index.search(encrypted_key)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module Snowden
2
+ VERSION = "0.9.0"
3
+ end
@@ -0,0 +1,39 @@
1
+ module Snowden
2
+ class WildcardGenerator
3
+ def initialize(args)
4
+ @edit_distance = args.fetch(:edit_distance)
5
+ end
6
+
7
+ # @api private
8
+ def wildcards(string)
9
+ wildcards = [string]
10
+ edit_distance.times do
11
+ wildcards = add_wildcard_layer(wildcards)
12
+ end
13
+
14
+ wildcards = wildcards.uniq
15
+
16
+ wildcards.to_enum
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :edit_distance
22
+
23
+ def add_wildcard_layer(list_of_strings)
24
+ list_of_strings.map {|s| base_wildcards(s) }.flatten
25
+ end
26
+
27
+ def base_wildcards(string)
28
+ string_range = (0..string.length)
29
+
30
+ [string]
31
+ .concat(string_range.map { |i| string.dup.insert(i, wildcard_char) })
32
+ .concat(string_range.map { |i| string.dup.tap { |s| s[i] = wildcard_char } })
33
+ end
34
+
35
+ def wildcard_char
36
+ "*"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'snowden/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "snowden"
8
+ spec.version = Snowden::VERSION
9
+ spec.authors = ["Sam Phippen", "Stephen Best"]
10
+ spec.email = ["samphippen@googlemail.com", "stephen@howareyou.com"]
11
+ spec.description = %q{Fuzzy encrypted indexes in ruby}
12
+ spec.summary = %q{Fuzzy encrypted indexes in ruby}
13
+ spec.homepage = "http://github.com/cambridge-healthcare/snowden"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "redis"
22
+ spec.add_dependency "hiredis"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ end
@@ -0,0 +1,47 @@
1
+ require "spec_helper"
2
+ require "redis"
3
+ require 'snowden'
4
+
5
+ describe "the examples from the readme" do
6
+ before do
7
+ Redis.new(:driver => :hiredis).flushdb
8
+ end
9
+
10
+ it "works for the example in the usage section" do
11
+ # 256 bit aes with 128 bit block
12
+ aes_key = "a"*(256/8)
13
+ aes_iv = "b"*(128/8)
14
+
15
+ index = Snowden.new_encrypted_index(aes_key, aes_iv, Snowden::Backends::HashBackend.new("",{}))
16
+ searcher = Snowden.new_encrypted_searcher(aes_key, aes_iv, index)
17
+
18
+ index.store("bacon", "bits")
19
+
20
+ # => ["bits"]
21
+ expect(searcher.search("bac")).to eq(["bits"])
22
+ end
23
+
24
+ it "works for the example in the redis section" do
25
+ redis = Redis.new(:driver => :hiredis)
26
+ redis_backend = Snowden::Backends::RedisBackend.new("index_namespace", redis)
27
+
28
+ aes_key = OpenSSL::Random.random_bytes(256/8)
29
+ aes_iv = OpenSSL::Random.random_bytes(128/8)
30
+
31
+ index = Snowden.new_encrypted_index(aes_key, aes_iv, redis_backend)
32
+ index.store("bacon", "bits")
33
+
34
+ searcher = Snowden.new_encrypted_searcher(aes_key, aes_iv, index)
35
+ expect(searcher.search("bac")).to eq(["bits"])
36
+ end
37
+
38
+ it "works for the cipher_spec example in the configuration section" do
39
+ Snowden.configuration.cipher_spec = "RC4"
40
+
41
+ aes_key = "a"*(256/8)
42
+ aes_iv = "b"*(128/8)
43
+
44
+ #Sometime later:
45
+ index = Snowden.new_encrypted_index(aes_key, aes_iv, Snowden::Backends::HashBackend.new("",{}))
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+ require "snowden"
3
+
4
+ module Snowden::Backends
5
+ describe HashBackend do
6
+ subject(:backend) { HashBackend.new("nomspace", hash) }
7
+ let(:hash) { {} }
8
+
9
+ describe "#save" do
10
+ it "returns nil" do
11
+ expect(backend.save(:key, :value)).to be nil
12
+ end
13
+ end
14
+
15
+ describe "#find" do
16
+ let(:hash) { {["nomspace", :key2] => :value2} }
17
+
18
+ it "finds the value stored under the key in the hash" do
19
+ expect(backend.find(:key2)).to eq(:value2)
20
+ end
21
+ end
22
+
23
+ describe "saving and finding" do
24
+ let(:hash) { {} }
25
+
26
+ it "can find values it has saved" do
27
+ backend.save(:key, :value)
28
+ expect(backend.find(:key)).to eq([:value])
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+ require "snowden"
3
+ require "redis_gun"
4
+
5
+ module Snowden::Backends
6
+ describe RedisBackend do
7
+ subject(:backend) { RedisBackend.new("namespace", redis_connection_helper) }
8
+ let(:redis_connection) { RedisGun::RedisServer.new.tap {|x| x.running? } }
9
+
10
+ describe "#save" do
11
+ it "returns nil" do
12
+ expect(backend.save("key", "value")).to be nil
13
+ end
14
+ end
15
+
16
+ describe "#find" do
17
+ it "finds the value stored under the key in redis" do
18
+ redis_connection_helper.lpush("namespace:bacon", "troll")
19
+ expect(backend.find("bacon")).to eq(["troll"])
20
+ end
21
+ end
22
+
23
+ describe "saving and finding" do
24
+ it "can find values it has saved" do
25
+ backend.save("key", "value")
26
+ expect(backend.find("key")).to eq(["value"])
27
+ end
28
+ end
29
+
30
+ def redis_connection_helper
31
+ Redis.new(:url => redis_connection.socket)
32
+ end
33
+
34
+ after do
35
+ redis_connection.stop
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,29 @@
1
+ require "spec_helper"
2
+ require "snowden"
3
+
4
+ module Snowden
5
+ describe Crypto do
6
+ subject(:crypto) {
7
+ Crypto.new(
8
+ :key => key,
9
+ :iv => iv,
10
+ :cipher_spec => cipher_spec,
11
+ :padding_size => padding_size,
12
+ )
13
+ }
14
+
15
+ let(:key) { "a"*(256/8) }
16
+ let(:iv) { "b"*(128/8) }
17
+
18
+ let(:cipher_spec) { "AES-256-CBC" }
19
+ let(:padding_size) { double(:padding_size) }
20
+
21
+ it "can encrypt data" do
22
+ expect(crypto.encrypt("asdf")).not_to be == "asdf"
23
+ end
24
+
25
+ it "can decrypt data that it encrypts" do
26
+ expect(crypto.decrypt(crypto.encrypt("asdf"))).to be == "asdf"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,43 @@
1
+ require "spec_helper"
2
+ require "snowden"
3
+
4
+ module Snowden
5
+ describe EncryptedSearchIndex do
6
+ subject(:index) {
7
+ EncryptedSearchIndex.new(
8
+ :crypto => crypto,
9
+ :backend => backend,
10
+ :wildcard_generator => wildcard_generator,
11
+ )
12
+ }
13
+
14
+ let(:crypto) { double("crypto") }
15
+ let(:key) { double("key") }
16
+ let(:value) { double("value") }
17
+ let(:wildcard_key) { double("wildcard key") }
18
+ let(:encrypted_wildcard_key) { double("encrypted wildcard key") }
19
+ let(:encrypted_value) { double("encrypted value") }
20
+ let(:backend) { double("backend", :save => nil) }
21
+ let(:wildcard_generator) { double("wildcard_generator") }
22
+
23
+
24
+ describe "#save" do
25
+ it "stores the wildcard and the encrypted value" do
26
+ allow(wildcard_generator).to receive(:wildcards).with(key).and_return([wildcard_key].to_enum)
27
+ allow(crypto).to receive(:encrypt).with(wildcard_key).and_return(encrypted_wildcard_key)
28
+ allow(crypto).to receive(:padded_encrypt).with(value).and_return(encrypted_value)
29
+
30
+ index.store(key, value)
31
+
32
+ expect(backend).to have_received(:save).with(encrypted_wildcard_key, encrypted_value)
33
+ end
34
+ end
35
+
36
+ describe "#search" do
37
+ it "retreives the value matching the passed encrypted value" do
38
+ allow(backend).to receive(:find).with(encrypted_wildcard_key).and_return(encrypted_value)
39
+ expect(index.search(encrypted_wildcard_key)).to be == encrypted_value
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,89 @@
1
+ require "spec_helper"
2
+ require "snowden"
3
+
4
+ module Snowden
5
+ describe EncryptedSearcher do
6
+ subject(:searcher) {
7
+ EncryptedSearcher.new(
8
+ :crypto => crypto,
9
+ :index => index,
10
+ :wildcard_generator => wildcard_generator,
11
+ )
12
+ }
13
+
14
+ let(:crypto) { double("crypto") }
15
+ let(:index) { double("index") }
16
+ let(:wildcard_generator) { double("wildcard generator") }
17
+
18
+ describe "#search" do
19
+ let(:search_string) { double(:search_string) }
20
+ let(:wildcard_search_string) { double(:wildcard_search_string) }
21
+ let(:encrypted_wildcard_search_string) { double(:encrypted_wildcard_search_string) }
22
+ let(:encrypted_value) { double(:encrypted_value) }
23
+ let(:decrypted_value) { double(:decrypted_value) }
24
+
25
+ it "returns the decrypted first matching encrypted value" do
26
+ allow(wildcard_generator).to receive(:wildcards)
27
+ .and_return([wildcard_search_string].to_enum)
28
+
29
+ allow(crypto).to receive(:encrypt)
30
+ .with(wildcard_search_string).and_return(encrypted_wildcard_search_string)
31
+
32
+ allow(crypto).to receive(:padded_decrypt)
33
+ .with(encrypted_value).and_return(decrypted_value)
34
+
35
+ allow(index).to receive(:search)
36
+ .with(encrypted_wildcard_search_string).and_return([encrypted_value])
37
+
38
+ expect(searcher.search(search_string)).to eq([decrypted_value])
39
+ end
40
+
41
+ context "with two matching encrypted values" do
42
+ let(:encrypted_value1) { double(:encrypted_value1) }
43
+ let(:decrypted_value1) { double(:decrypted_value1) }
44
+ let(:encrypted_value2) { double(:encrypted_value2) }
45
+ let(:decrypted_value2) { double(:decrypted_value2) }
46
+
47
+ it "returns both decrypted values" do
48
+ allow(wildcard_generator).to receive(:wildcards)
49
+ .and_return([wildcard_search_string].to_enum)
50
+
51
+ allow(crypto).to receive(:encrypt)
52
+ .with(wildcard_search_string).and_return(encrypted_wildcard_search_string)
53
+
54
+ allow(crypto).to receive(:padded_decrypt)
55
+ .with(encrypted_value1).and_return(decrypted_value1)
56
+
57
+ allow(index).to receive(:search)
58
+ .with(encrypted_wildcard_search_string).and_return([encrypted_value1, encrypted_value2])
59
+
60
+ allow(crypto).to receive(:padded_decrypt)
61
+ .with(encrypted_value2).and_return(decrypted_value2)
62
+
63
+ expect(searcher.search(search_string)).to eq([decrypted_value1, decrypted_value2])
64
+ end
65
+
66
+ context "when the values are the same" do
67
+ let(:encrypted_value1) { double(:encrypted_value1) }
68
+ let(:decrypted_value1) { double(:decrypted_value1) }
69
+
70
+ it "returns both decrypted values" do
71
+ allow(wildcard_generator).to receive(:wildcards)
72
+ .and_return([wildcard_search_string].to_enum)
73
+
74
+ allow(crypto).to receive(:encrypt)
75
+ .with(wildcard_search_string).and_return(encrypted_wildcard_search_string)
76
+
77
+ allow(index).to receive(:search)
78
+ .with(encrypted_wildcard_search_string).and_return([encrypted_value1, encrypted_value1])
79
+
80
+ allow(crypto).to receive(:padded_decrypt)
81
+ .with(encrypted_value1).and_return(decrypted_value1).exactly(2).times
82
+
83
+ expect(searcher.search(search_string)).to eq([decrypted_value1])
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,27 @@
1
+ require "spec_helper"
2
+ require "snowden"
3
+
4
+ module Snowden
5
+ describe WildcardGenerator do
6
+ let(:wildcard_generator) { WildcardGenerator.new(:edit_distance => 2) }
7
+
8
+ let(:wildcards_fixture) { [
9
+ "a", "*a", "a*", "*", "**a", "*a*", "**", "a**"
10
+ ].sort }
11
+
12
+ describe "#each_wildcard" do
13
+ it "yields to the passed block" do
14
+ called = false
15
+ wildcard_generator.wildcards("a").each do
16
+ called = true
17
+ end
18
+
19
+ expect(called).to be true
20
+ end
21
+
22
+ it "gives back some wildcards" do
23
+ expect(wildcard_generator.wildcards("a").to_a.sort).to eq(wildcards_fixture)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+ require "snowden"
3
+
4
+ describe Snowden do
5
+ let(:key) { "a"*(256/8) }
6
+ let(:iv) { "b"*(128/8) }
7
+
8
+ describe ".new_encrypted_index" do
9
+ subject(:index) { Snowden.new_encrypted_index(
10
+ key,
11
+ iv,
12
+ Snowden::Backends::HashBackend.new({})
13
+ )
14
+ }
15
+
16
+ it "gives back an index" do
17
+ expect(index).to be_a_kind_of Snowden::EncryptedSearchIndex
18
+ end
19
+
20
+ it "builds an index that does crypto" do
21
+ subject.store("encrypt me", "please")
22
+
23
+ encrypted_value = index.search(encrypt_helper("encrypt me")).first
24
+ decrypted_value = padded_decrypt_helper(encrypted_value)
25
+
26
+ expect(decrypted_value).to eq("please")
27
+ end
28
+ end
29
+
30
+ describe ".new_encrypted_searcher" do
31
+ let(:index) { Snowden.new_encrypted_index(
32
+ key,
33
+ iv,
34
+ Snowden::Backends::HashBackend.new({})
35
+ )
36
+ }
37
+ subject(:searcher) { Snowden.new_encrypted_searcher(key, iv, index) }
38
+
39
+ it "gives back a searcher" do
40
+ expect(subject).to be_a_kind_of Snowden::EncryptedSearcher
41
+ end
42
+
43
+ it "builds a searcher that can find values in the index" do
44
+ index.store("sam", "12345")
45
+ index.store("gerhard", "pony")
46
+
47
+ expect(searcher.search("gerha")).to eq(["pony"])
48
+ end
49
+ end
50
+
51
+ def encrypt_helper(value)
52
+ Snowden.crypto_for(key, iv).encrypt(value)
53
+ end
54
+
55
+ def padded_decrypt_helper(value)
56
+ Snowden.crypto_for(key, iv).padded_decrypt(value)
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require 'simplecov'
9
+ SimpleCov.start
10
+
11
+ RSpec.configure do |config|
12
+ config.run_all_when_everything_filtered = true
13
+ config.filter_run :focus
14
+
15
+ config.order = 'random'
16
+
17
+ config.mock_with :rspec do |configuration|
18
+ configuration.syntax = :expect
19
+ end
20
+
21
+ config.expect_with :rspec do |configuration|
22
+ configuration.syntax = :expect
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snowden
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sam Phippen
9
+ - Stephen Best
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-09-20 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: redis
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: hiredis
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: '1.3'
63
+ - !ruby/object:Gem::Dependency
64
+ name: rake
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ description: Fuzzy encrypted indexes in ruby
80
+ email:
81
+ - samphippen@googlemail.com
82
+ - stephen@howareyou.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - .gitignore
88
+ - .rspec
89
+ - Gemfile
90
+ - LICENSE.txt
91
+ - README.md
92
+ - Rakefile
93
+ - lib/snowden.rb
94
+ - lib/snowden/backends/hash_backend.rb
95
+ - lib/snowden/backends/redis_backend.rb
96
+ - lib/snowden/configuration.rb
97
+ - lib/snowden/crypto.rb
98
+ - lib/snowden/encrypted_search_index.rb
99
+ - lib/snowden/encrypted_searcher.rb
100
+ - lib/snowden/version.rb
101
+ - lib/snowden/wildcard_generator.rb
102
+ - snowden.gemspec
103
+ - spec/readme_spec.rb
104
+ - spec/snowden/backends/hash_backend_spec.rb
105
+ - spec/snowden/backends/redis_backend_spec.rb
106
+ - spec/snowden/crypto_spec.rb
107
+ - spec/snowden/encrypted_search_index_spec.rb
108
+ - spec/snowden/encrypted_searcher_spec.rb
109
+ - spec/snowden/wildcard_generator_spec.rb
110
+ - spec/snowden_spec.rb
111
+ - spec/spec_helper.rb
112
+ homepage: http://github.com/cambridge-healthcare/snowden
113
+ licenses:
114
+ - MIT
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ segments:
126
+ - 0
127
+ hash: -3778383592789494811
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ segments:
135
+ - 0
136
+ hash: -3778383592789494811
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.25
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: Fuzzy encrypted indexes in ruby
143
+ test_files:
144
+ - spec/readme_spec.rb
145
+ - spec/snowden/backends/hash_backend_spec.rb
146
+ - spec/snowden/backends/redis_backend_spec.rb
147
+ - spec/snowden/crypto_spec.rb
148
+ - spec/snowden/encrypted_search_index_spec.rb
149
+ - spec/snowden/encrypted_searcher_spec.rb
150
+ - spec/snowden/wildcard_generator_spec.rb
151
+ - spec/snowden_spec.rb
152
+ - spec/spec_helper.rb
153
+ has_rdoc: