snowden 0.9.0
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.
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +151 -0
- data/Rakefile +1 -0
- data/lib/snowden.rb +76 -0
- data/lib/snowden/backends/hash_backend.rb +41 -0
- data/lib/snowden/backends/redis_backend.rb +42 -0
- data/lib/snowden/configuration.rb +28 -0
- data/lib/snowden/crypto.rb +46 -0
- data/lib/snowden/encrypted_search_index.rb +53 -0
- data/lib/snowden/encrypted_searcher.rb +40 -0
- data/lib/snowden/version.rb +3 -0
- data/lib/snowden/wildcard_generator.rb +39 -0
- data/snowden.gemspec +26 -0
- data/spec/readme_spec.rb +47 -0
- data/spec/snowden/backends/hash_backend_spec.rb +33 -0
- data/spec/snowden/backends/redis_backend_spec.rb +38 -0
- data/spec/snowden/crypto_spec.rb +29 -0
- data/spec/snowden/encrypted_search_index_spec.rb +43 -0
- data/spec/snowden/encrypted_searcher_spec.rb +89 -0
- data/spec/snowden/wildcard_generator_spec.rb +27 -0
- data/spec/snowden_spec.rb +58 -0
- data/spec/spec_helper.rb +24 -0
- metadata +153 -0
data/.gitignore
ADDED
data/.rspec
ADDED
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"
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/snowden.rb
ADDED
@@ -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,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
|
data/snowden.gemspec
ADDED
@@ -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
|
data/spec/readme_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|