simply-aes 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.githooks/pre-commit/run-ruby-appraiser +23 -0
- data/.githooks/pre-commit/run-specs +21 -0
- data/.gitignore +22 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +7 -0
- data/CONTRIBUTING.md +31 -0
- data/Gemfile +4 -0
- data/LICENSE.md +13 -0
- data/README.md +106 -0
- data/Rakefile +3 -0
- data/lib/simply-aes.rb +15 -0
- data/lib/simply-aes/cipher.rb +122 -0
- data/lib/simply-aes/cipher/error.rb +22 -0
- data/lib/simply-aes/format.rb +97 -0
- data/lib/simply-aes/version.rb +6 -0
- data/simply-aes.gemspec +25 -0
- data/spec/simply-aes/cipher_spec.rb +101 -0
- data/spec/simply-aes/format_spec.rb +52 -0
- metadata +131 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
if [ "$1" == '--about' ]; then
|
3
|
+
echo 'Validates staged changes with ruby-appraiser'
|
4
|
+
exit 0
|
5
|
+
fi
|
6
|
+
|
7
|
+
echo -e "\033[0;36mRuby Appraiser: running\033[0m"
|
8
|
+
bundle exec ruby-appraiser rubocop --mode=staged
|
9
|
+
result_code=$?
|
10
|
+
if [ $result_code -gt "0" ]; then
|
11
|
+
echo -en "\033[0;31m" # RED
|
12
|
+
echo "[✘] Ruby Appraiser found newly-created defects and "
|
13
|
+
echo " has blocked your commit."
|
14
|
+
echo " Fix the defects and commit again."
|
15
|
+
echo " To bypass, commit again with --no-verify."
|
16
|
+
echo -en "\033[0m" # RESET
|
17
|
+
exit $result_code
|
18
|
+
else
|
19
|
+
echo -en "\033[0;32m" # GREEN
|
20
|
+
echo "[✔] Ruby Appraiser ok"
|
21
|
+
echo -en "\033[0m" #RESET
|
22
|
+
fi
|
23
|
+
exit
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
if [ "$1" == '--about' ]; then
|
3
|
+
echo 'Runs project specs'
|
4
|
+
exit 0
|
5
|
+
fi
|
6
|
+
|
7
|
+
echo -e "\033[0;36mPre-commit hook: running specs...\033[0m"
|
8
|
+
bundle exec rake spec
|
9
|
+
result_code=$?
|
10
|
+
if [ $result_code -gt "0" ]; then
|
11
|
+
echo -en "\033[0;31m" # RED
|
12
|
+
echo "[✘] Specs have failed and blocked your commit."
|
13
|
+
echo " Fix the defects and try again."
|
14
|
+
echo " To bypass, commit again with --no-verify."
|
15
|
+
echo -en "\033[0m" # RESET
|
16
|
+
exit $result_code
|
17
|
+
else
|
18
|
+
echo -en "\033[0;32m" # GREEN
|
19
|
+
echo "[✔] Specs ok"
|
20
|
+
echo -en "\033[0m" #RESET
|
21
|
+
fi
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
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
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
`SimplyAES` is [Apache-2-liennsed](LICENSE.md) and community contributions are welcome.
|
4
|
+
|
5
|
+
## Git-Flow
|
6
|
+
|
7
|
+
`SimplyAES` follows the [git-flow][] branching model, which means that every commit on `master` is a release.
|
8
|
+
The default working branch is `develop`, so in general please keep feature pull-requests based against the current `develop`.
|
9
|
+
Hotfixes -- fixes to already-released builds -- should be based on `master` or the appropriate major version's maintenance branch.
|
10
|
+
|
11
|
+
- ensure your issue is not already addressed in an issue or pull-request
|
12
|
+
- fork simply-aes
|
13
|
+
- use the git-flow model to start your feature or hotfix
|
14
|
+
- make some commits (please include specs)
|
15
|
+
- submit a pull-request
|
16
|
+
|
17
|
+
## Bug Reporting
|
18
|
+
|
19
|
+
Please include clear steps-to-reproduce.
|
20
|
+
Spec files are especially welcome; a failing spec can be contributed as a pull-request against `develop`.
|
21
|
+
|
22
|
+
## Ruby Appraiser
|
23
|
+
|
24
|
+
`SimplyAES` uses the [ruby-appraiser][] gem via [pre-commit][] hook, which can be activated by installing [icefox/git-hooks][] and running `git-hooks --install` while in the repo.
|
25
|
+
Rubocop supplies strong guidelines;
|
26
|
+
use them to reduce defects as much as you can, but if you believe clarity will be sacrificed they can be bypassed with the `--no-verify` flag.
|
27
|
+
|
28
|
+
[git-flow]: http://nvie.com/posts/a-successful-git-branching-model/
|
29
|
+
[pre-commit]: .githooks/pre-commit/run-ruby-appraiser
|
30
|
+
[ruby-appraiser]: https://github.com/simplymeasured/ruby-appraiser
|
31
|
+
[icefox/git-hooks]: https://github.com/icefox/git-hooks
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2013 Simply Measured
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
Simply AES
|
2
|
+
==========
|
3
|
+
|
4
|
+
Proper cryptography is easy to get wrong, and the Ruby stdlib adapters to OpenSSL's implentations of various encryption algorithms are often cumbersome to work with, expecting the developer to understand a great deal of terminology.
|
5
|
+
|
6
|
+
SimplyAES provides a simple, straight-forward interface for securely encrypting data in Ruby using key-based AES-256, the most secure variant of the [*Advanced Encryption Standard*][AES], complete with securely-generated initialisation vectors.
|
7
|
+
|
8
|
+
[AES]: http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
|
9
|
+
|
10
|
+
Installation
|
11
|
+
------------
|
12
|
+
|
13
|
+
SimplyAES is available on rubygems.org:
|
14
|
+
|
15
|
+
`gem install simply-aes`
|
16
|
+
|
17
|
+
Basic Usage
|
18
|
+
-----------
|
19
|
+
|
20
|
+
~~~ ruby
|
21
|
+
require 'simply-aes'
|
22
|
+
|
23
|
+
# Create a Cipher object; unless provided,
|
24
|
+
# a secure-random key will be generated.
|
25
|
+
cipher = SimplyAES.new # => <SimplyAES::Cipher:70293125591140>
|
26
|
+
|
27
|
+
# By default, the Cipher uses the Bytes formatter,
|
28
|
+
# so byte data is emitted as a string of raw bytes.
|
29
|
+
cipher.key # => "\x91\xD4\xEA=-\xC1\xB6\xE3\xDBP&\xDC\xB6\xFE\xDA\xF1\xF0L\x8Fz\e\xF7k]\x15\x9A\x9B8\xB1\xF3\xE3\xEE"
|
30
|
+
|
31
|
+
# All public methods take a `:format` option,
|
32
|
+
# which can be used to provide a format-helper;
|
33
|
+
# let's use `hex` which is easier to work with:
|
34
|
+
cipher.key(format: :hex) # => "91d4ea3d2dc1b6e3db5026dcb6fedaf1f04c8f7a1bf76b5d159a9b38b1f3e3ee"
|
35
|
+
|
36
|
+
# We can encrypt strings very easily:
|
37
|
+
secret = cipher.dump('Hello, World!', format: :hex) # => "521735c29ca1a6ae1a8fca49a9fb28ed8bf5d1bce3b39eb0286ea9c6b5dc286f"
|
38
|
+
|
39
|
+
# We can also decrypt strings; here,
|
40
|
+
# the format argument tells us how
|
41
|
+
# the ciphertext is formatted:
|
42
|
+
cipher.load(secret, format: :hex) # => 'Hello, World!'
|
43
|
+
|
44
|
+
# Attempting to load a ciphertext with an incorrect key emits an exception:
|
45
|
+
SimplyAES.new.load(secret, format: :hex) # !> SimplyAES::Cipher::LoadError
|
46
|
+
~~~
|
47
|
+
|
48
|
+
Interoperability
|
49
|
+
----------------
|
50
|
+
|
51
|
+
The ciphertext from SimplyAES can be decrypted easily by _any_ AES-compliant tool or library, with the following hints:
|
52
|
+
|
53
|
+
In SimplyAES, a secure-random initialisation vector is generated for each encryption unless explicitly given, and the 16-byte IV is returned with the ciphertext;
|
54
|
+
this means that two identical strings encrypted with the same key will have substantially different representations, making it harder for an attacker to correlate encrypted data.
|
55
|
+
Because the IV does not need to be kept [secret][iv-requirements], SimplyAES emits the iv+ciphertext as a single byte string.
|
56
|
+
|
57
|
+
~~~
|
58
|
+
+-------- 16-byte IV ----------++--- unbounded payload size --->
|
59
|
+
| ||
|
60
|
+
521735c29ca1a6ae1a8fca49a9fb28ed8bf5d1bce3b39eb0286ea9c6b5dc286f
|
61
|
+
~~~
|
62
|
+
|
63
|
+
To decrypt AES ciphertext that has been encrypted using another library, simply prepend the IV (in the same format as the encrypted data) to the ciphertext:
|
64
|
+
|
65
|
+
~~~ ruby
|
66
|
+
ciphertext = '8bf5d1bce3b39eb0286ea9c6b5dc286f'
|
67
|
+
iv = '521735c29ca1a6ae1a8fca49a9fb28ed'
|
68
|
+
|
69
|
+
cipher.load(iv+ciphertext, format: :hex) # => 'Hello, World!'
|
70
|
+
~~~
|
71
|
+
|
72
|
+
To decrypt a SimplyAES iv+ciphertext payload, simply use the first 16 bytes as the IV, and the remaining as the ciphertext.
|
73
|
+
|
74
|
+
~~~ ruby
|
75
|
+
# encoding: BINARY
|
76
|
+
key = "\x91\xD4\xEA=-\xC1\xB6\xE3\xDBP&\xDC\xB6\xFE\xDA\xF1\xF0L\x8Fz\e\xF7k]\x15\x9A\x9B8\xB1\xF3\xE3\xEE"
|
77
|
+
|
78
|
+
payload = "R\x175\xC2\x9C\xA1\xA6\xAE\x1A\x8F\xCAI\xA9\xFB(\xED\x8B\xF5\xD1\xBC\xE3\xB3\x9E\xB0(n\xA9\xC6\xB5\xDC(o"
|
79
|
+
iv = payload[0...16] # => "R\x175\xC2\x9C\xA1\xA6\xAE\x1A\x8F\xCAI\xA9\xFB(\xED"
|
80
|
+
ciphertext = payload[16..-1] # => "\x8B\xF5\xD1\xBC\xE3\xB3\x9E\xB0(n\xA9\xC6\xB5\xDC(o"
|
81
|
+
|
82
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
83
|
+
cipher.decrypt
|
84
|
+
cipher.key = key
|
85
|
+
cipher.iv = iv
|
86
|
+
|
87
|
+
decrypted = (cipher.update(ciphertext) + cipher.final) # => 'Hello, World!'
|
88
|
+
~~~
|
89
|
+
|
90
|
+
Advanced Usage
|
91
|
+
--------------
|
92
|
+
|
93
|
+
~~~ ruby
|
94
|
+
require 'simply-aes'
|
95
|
+
|
96
|
+
# The Cipher can be initialized with a default formatter:
|
97
|
+
cipher = SimplyAES.new(format: :base64)
|
98
|
+
|
99
|
+
# All byte-data respects the default format, unless overridden.
|
100
|
+
cipher.key # => "Okve+PUasFuqB7zONT3XgCz0adJN3a6gr58k+/rve1E="
|
101
|
+
~~~
|
102
|
+
|
103
|
+
License
|
104
|
+
-------
|
105
|
+
|
106
|
+
SimplyAES is Apache 2 Licensed.
|
data/Rakefile
ADDED
data/lib/simply-aes.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'simply-aes/version'
|
4
|
+
require 'simply-aes/cipher'
|
5
|
+
|
6
|
+
# SimplyAES provides a simple, straight-forward interface for securely
|
7
|
+
# encrypting data in Ruby using key-based AES-256, the most secure variant of
|
8
|
+
# the [*Advanced Encryption Standard*][AES], complete with securely-generated
|
9
|
+
# initialisation vectors.
|
10
|
+
module SimplyAES
|
11
|
+
# @see SimplyAES::Cipher#initialize
|
12
|
+
def self.new(*args)
|
13
|
+
Cipher.new(*args)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'simply-aes/format'
|
4
|
+
require 'simply-aes/cipher/error'
|
5
|
+
|
6
|
+
require 'openssl'
|
7
|
+
|
8
|
+
module SimplyAES
|
9
|
+
# The Cipher is the heart of SimplyAES, and can be used to load ciphertext or
|
10
|
+
# to dump a string's ciphertext with a given key or a securely-generated one.
|
11
|
+
class Cipher
|
12
|
+
# @overload initialize(options)
|
13
|
+
# @overload initialize(key, options)
|
14
|
+
# @param key [String] a 32-byte (256-bit) string
|
15
|
+
# If not provided, a secure random key will be generated
|
16
|
+
# @param options [Hash{Symbol=>Object}]
|
17
|
+
# @option options [Symbol, SimplyAES::Format] (:bytes)
|
18
|
+
# The format is used to load provided data, including the given key,
|
19
|
+
# and as a default encoder/decoder of encrypted data; this can be
|
20
|
+
# overridden in the #load and #dump methods.
|
21
|
+
# @raise [SimplyAES::Cipher::Error]
|
22
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
23
|
+
def initialize(*args)
|
24
|
+
options = (args.last.is_a?(Hash) ? args.pop.dup : {})
|
25
|
+
|
26
|
+
@format = Format[options.delete(:format) { :bytes }]
|
27
|
+
|
28
|
+
# extract the given key, or securely generate one
|
29
|
+
@key = format.load(args.pop) unless args.empty?
|
30
|
+
@key ||= native_cipher.random_key
|
31
|
+
|
32
|
+
# validate initialisation
|
33
|
+
fail(ArgumentError, 'invalid key length') unless @key.bytesize == 32
|
34
|
+
fail(ArgumentError, 'wrong number of arguments') unless args.empty?
|
35
|
+
fail(ArgumentError, "unknown options: #{options}") unless options.empty?
|
36
|
+
rescue => err
|
37
|
+
raise Error, "failed to initialize #{self.class.name} (#{err})"
|
38
|
+
end
|
39
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
40
|
+
|
41
|
+
# @param options [Hash{Symbol=>Object}]
|
42
|
+
# @option options [Symbol] :format (default: self.format)
|
43
|
+
# @return [String] formatted string
|
44
|
+
def key(options = {})
|
45
|
+
format(options).dump(@key.dup)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param plaintext [String]
|
49
|
+
# @param options [Hash{Symbol=>Object}]
|
50
|
+
# @option options [String] :iv (default: secure random iv)
|
51
|
+
# up to 16 bytes, used as an initialisation vector
|
52
|
+
# @option options [Symbol] :format (default: self.format)
|
53
|
+
# @return iv_ciphertext [String] binary string
|
54
|
+
# @raise SimplyAES::Cipher::DumpError
|
55
|
+
def dump(plaintext, options = {})
|
56
|
+
encipher = native_cipher(:encrypt)
|
57
|
+
|
58
|
+
# ensure a 16-byte initialisation vector
|
59
|
+
iv = options.fetch(:iv) { encipher.random_iv }
|
60
|
+
fail(ArgumentError, 'iv must be 16 bytes') unless iv.bytesize == 16
|
61
|
+
encipher.iv = iv
|
62
|
+
|
63
|
+
ciphertext = encipher.update(plaintext) + encipher.final
|
64
|
+
|
65
|
+
format(options).dump(iv + ciphertext)
|
66
|
+
rescue => err
|
67
|
+
raise DumpError, err.message
|
68
|
+
end
|
69
|
+
alias_method(:encrypt, :dump)
|
70
|
+
|
71
|
+
# @param iv_ciphertext [String]
|
72
|
+
# @option options [Symbol] :format (default: self.format)
|
73
|
+
# @return plaintext [String]
|
74
|
+
# @raise SimplyAES::Cipher::LoadError
|
75
|
+
def load(iv_ciphertext, options = {})
|
76
|
+
@key || fail(ArgumentError, 'key not provided!')
|
77
|
+
|
78
|
+
# if the IV is given as an argument, inject it to the ciphertext
|
79
|
+
given_iv = options[:iv]
|
80
|
+
given_iv && (iv_ciphertext = given_iv + iv_ciphertext)
|
81
|
+
|
82
|
+
# shift the 16-byte initialisation vector from the front
|
83
|
+
iv, ciphertext = format(options).load(iv_ciphertext).unpack('a16a*')
|
84
|
+
|
85
|
+
decipher = native_cipher(:decrypt)
|
86
|
+
decipher.iv = iv
|
87
|
+
|
88
|
+
decipher.update(ciphertext) + decipher.final
|
89
|
+
rescue => err
|
90
|
+
raise LoadError, err.message
|
91
|
+
end
|
92
|
+
alias_method(:decrypt, :load)
|
93
|
+
|
94
|
+
# @api private
|
95
|
+
# @return [String]
|
96
|
+
def inspect
|
97
|
+
"<#{self.class.name}:#{__id__}>"
|
98
|
+
end
|
99
|
+
|
100
|
+
# @api private
|
101
|
+
# @return [SimplyAES::Format]
|
102
|
+
def format(options = {})
|
103
|
+
Format[options.fetch(:format) { @format }]
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# Returns an AES-256-CBC OpenSSL::Cipher object pre-configured with the
|
109
|
+
# requested mode and our key; used internally in initialize, load,
|
110
|
+
# and dump.
|
111
|
+
#
|
112
|
+
# @api private
|
113
|
+
# @param mode [:encode, :decode]
|
114
|
+
# @return [OpenSSL::Cipher]
|
115
|
+
def native_cipher(mode = nil)
|
116
|
+
::OpenSSL::Cipher.new('AES-256-CBC').tap do |cipher|
|
117
|
+
mode && cipher.public_send(mode)
|
118
|
+
@key && cipher.key = @key
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module SimplyAES
|
4
|
+
# @see SimplyAES::Cipher
|
5
|
+
class Cipher
|
6
|
+
# SimplyAES::Cipher::Error is a wrapper for all
|
7
|
+
# errors raised by SimplyAES::Cipher
|
8
|
+
class Error < RuntimeError
|
9
|
+
# Back-port Ruby 2.1's Exception#cause
|
10
|
+
unless method_defined?(:cause)
|
11
|
+
def initialize(*args)
|
12
|
+
@cause = $! # rubocop:disable Style/SpecialGlobalVars
|
13
|
+
super
|
14
|
+
end
|
15
|
+
attr_reader :cause
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
LoadError = Class.new(Error)
|
20
|
+
DumpError = Class.new(Error)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# rubocop:disable Style/ModuleFunction
|
3
|
+
|
4
|
+
module SimplyAES
|
5
|
+
# Implementations of SimplyAES::Format are formatting helpers,
|
6
|
+
# used by SimplyAES::Cipher to dump byte-strings and to load
|
7
|
+
# formatted byte-strings.
|
8
|
+
module Format
|
9
|
+
# @param formatted [String]
|
10
|
+
# @return bytestring [String]
|
11
|
+
def load(formatted)
|
12
|
+
return super if defined?(super)
|
13
|
+
fail(NotImplementedError)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param bytestring [String]
|
17
|
+
# @return formatted [String]
|
18
|
+
def dump(bytestring)
|
19
|
+
return super if defined?(super)
|
20
|
+
fail(NotImplementedError)
|
21
|
+
end
|
22
|
+
|
23
|
+
@implementations = {}
|
24
|
+
|
25
|
+
def self.included(implementation)
|
26
|
+
if (name = implementation.name)
|
27
|
+
# @todo support alternate-paths
|
28
|
+
short_name = name.split('::').last.downcase.to_sym
|
29
|
+
self[short_name] = implementation
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.[]=(name, implementation)
|
34
|
+
fail(ArgumentError, "not a #{self}") unless implementation <= self
|
35
|
+
@implementations[name] = implementation
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.[](name)
|
39
|
+
return name if name.is_a?(Module) && name <= self
|
40
|
+
|
41
|
+
@implementations.fetch(name) do
|
42
|
+
fail(ArgumentError, "Unknown format: #{name}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# A Base64 implementation of SimplyAES::Format that emits
|
47
|
+
# strings *without* newlines and can handle concatenated-b64 strings
|
48
|
+
module Base64
|
49
|
+
extend self
|
50
|
+
require 'base64'
|
51
|
+
include Format
|
52
|
+
|
53
|
+
def load(formatted)
|
54
|
+
# Because Base64 has 3:4 raw:formated ratio, it doesn't always break
|
55
|
+
# cleanly on byte boundaries; add support for concatenated
|
56
|
+
# iv+ciphertext encoded payloads
|
57
|
+
formatted.scan(/[^=]+(?:=+|\Z)/m).map do |chunk|
|
58
|
+
::Base64.decode64(chunk)
|
59
|
+
end.join
|
60
|
+
end
|
61
|
+
|
62
|
+
def dump(bytestring)
|
63
|
+
::Base64.encode64(bytestring).tr("\n", '')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# A Hex implementation of SimplyAES::Format that emits
|
68
|
+
# strings *without* newlines and can handle concatenated-hex strings
|
69
|
+
module Hex
|
70
|
+
extend self
|
71
|
+
include Format
|
72
|
+
|
73
|
+
def load(formatted)
|
74
|
+
[formatted].pack('H*')
|
75
|
+
end
|
76
|
+
|
77
|
+
def dump(bytestring)
|
78
|
+
bytestring.unpack('H*')[0]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# The default implementation of SimplyAES::Format that reads and emits
|
83
|
+
# unformatted byte strings.
|
84
|
+
module Bytes
|
85
|
+
extend self
|
86
|
+
include Format
|
87
|
+
|
88
|
+
def load(formatted)
|
89
|
+
formatted.dup
|
90
|
+
end
|
91
|
+
|
92
|
+
def dump(bytestring)
|
93
|
+
bytestring.dup
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/simply-aes.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'simply-aes/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'simply-aes'
|
9
|
+
spec.version = SimplyAES::VERSION
|
10
|
+
spec.authors = ['Ryan Biesemeyer']
|
11
|
+
spec.email = ['ryan@simplymeasured.com']
|
12
|
+
spec.summary = 'Simple AES-256-driven encryption'
|
13
|
+
spec.homepage = 'https://github.com/simplymeasured/simply-aes'
|
14
|
+
spec.license = 'Apache 2'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
24
|
+
spec.add_development_dependency 'ruby-appraiser-rubocop'
|
25
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: BINARY (prevent string literals from being trans-coded to UTF-8)
|
2
|
+
|
3
|
+
require 'simply-aes/cipher'
|
4
|
+
|
5
|
+
describe(SimplyAES::Cipher) do
|
6
|
+
# default keys
|
7
|
+
let(:key_hex) do
|
8
|
+
'c3e6e6bbca5846ba4c1a4dd1953ccdc8a4e3fb0bfde7df8673de4f4f920e8df2'
|
9
|
+
end
|
10
|
+
let(:key) { SimplyAES::Format::Hex.load(key_hex) }
|
11
|
+
|
12
|
+
context '#key' do
|
13
|
+
context 'when initialised with a key' do
|
14
|
+
let(:cipher) { described_class.new(key_hex, format: :hex) }
|
15
|
+
|
16
|
+
subject { cipher }
|
17
|
+
|
18
|
+
it 'returns that key' do
|
19
|
+
expect(cipher.key).to eq(key_hex)
|
20
|
+
end
|
21
|
+
context('when a format is given') do
|
22
|
+
it 'formats the key' do
|
23
|
+
expect(cipher.key(format: :bytes)).to eq(key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when initialised without a key' do
|
29
|
+
let(:cipher) { described_class.new(format: :hex) }
|
30
|
+
it 'returns a securely-generated key' do
|
31
|
+
expect(cipher.key).to match(/[0-9a-f]{64}/)
|
32
|
+
end
|
33
|
+
it 'memoizes the securely-generated key' do
|
34
|
+
expect(cipher.key).to eq(cipher.key)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context '#dump' do
|
40
|
+
let(:cipher) { described_class.new(key_hex, format: :hex) }
|
41
|
+
|
42
|
+
# in order to make the output predictable, we need to use our own IV
|
43
|
+
context 'when an initialisation vector is given' do
|
44
|
+
let(:iv_hex) { 'babad50bea6035da71e9a0e076076860' }
|
45
|
+
let(:iv) { SimplyAES::Format::Hex.load(iv_hex) }
|
46
|
+
let(:decrypted) { 'Hello, World!' }
|
47
|
+
let(:result) { cipher.dump(decrypted, iv: iv, format: :bytes) }
|
48
|
+
|
49
|
+
context 'the encrypted value' do
|
50
|
+
subject { result }
|
51
|
+
it 'is prefixed by the IV' do
|
52
|
+
expect(result).to start_with(iv)
|
53
|
+
end
|
54
|
+
it 'is longer than the IV' do
|
55
|
+
expect(result.size).to be > (iv.size)
|
56
|
+
end
|
57
|
+
it 'matches the known value for the given key/iv pair' do
|
58
|
+
expect(result).to eq(
|
59
|
+
"\xBA\xBA\xD5\v\xEA`5\xDAq\xE9\xA0\xE0v\ah`\xE8\xCB\xF7+q\xB6" \
|
60
|
+
"\xA5\xF5\xCC\xAD\xB7\xEB\xA8\xB5s\xCD")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'dumping the same value multiple times' do
|
66
|
+
let(:decrypted) { 'Hello, World!' }
|
67
|
+
|
68
|
+
it 'emits different results, with different IVs' do
|
69
|
+
first = cipher.dump(decrypted, format: :bytes)
|
70
|
+
second = cipher.dump(decrypted, format: :bytes)
|
71
|
+
|
72
|
+
# @todo: better quantify "different"
|
73
|
+
expect(first).to_not eq(second)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context '#load' do
|
79
|
+
let(:cipher) { described_class.new(key_hex, format: :hex) }
|
80
|
+
let(:decrypted) { 'Hello, World!' }
|
81
|
+
let(:encrypted) do
|
82
|
+
"\xBA\xBA\xD5\v\xEA`5\xDAq\xE9\xA0\xE0v\ah`\xE8\xCB\xF7+q\xB6" \
|
83
|
+
"\xA5\xF5\xCC\xAD\xB7\xEB\xA8\xB5s\xCD"
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when given a value that had been encrypted with the same key' do
|
87
|
+
it 'decrypts the value' do
|
88
|
+
expect(cipher.load(encrypted, format: :bytes)).to eq(decrypted)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
context 'when given a value that was NOT encrypted with the same key' do
|
92
|
+
let(:alternate_cipher) { SimplyAES::Cipher.new(format: :bytes) }
|
93
|
+
let(:alternate_encrypted) { alternate_cipher.dump(decrypted) }
|
94
|
+
it 'raises a SimplyAES::Cipher::LoadError' do
|
95
|
+
expect do
|
96
|
+
cipher.load(alternate_encrypted, format: :bytes)
|
97
|
+
end.to raise_exception(SimplyAES::Cipher::LoadError)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: BINARY (prevent string literals from being trans-coded to UTF-8)
|
2
|
+
|
3
|
+
require 'simply-aes/format'
|
4
|
+
|
5
|
+
shared_examples_for(SimplyAES::Format) do
|
6
|
+
let(:format) { described_class } # e.g., described_module
|
7
|
+
subject { format }
|
8
|
+
it { is_expected.to respond_to :load }
|
9
|
+
it { is_expected.to respond_to :dump }
|
10
|
+
|
11
|
+
context '#load' do
|
12
|
+
it 'loads the value' do
|
13
|
+
expect(format.load(formatted)).to eq(bytestring)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context '#dump' do
|
18
|
+
it 'dumps the value' do
|
19
|
+
expect(format.dump(bytestring)).to eq(formatted)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'implementation selecting' do
|
24
|
+
it 'can be loaded by name' do
|
25
|
+
expect(SimplyAES::Format[short_name]).to eq format
|
26
|
+
end
|
27
|
+
it 'can be loaded when given explicitly' do
|
28
|
+
expect(SimplyAES::Format[format]).to eq format
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe(SimplyAES::Format::Bytes) do
|
34
|
+
let(:formatted) { "\xff\x1c\xae" }
|
35
|
+
let(:bytestring) { "\xff\x1c\xae" }
|
36
|
+
let(:short_name) { :bytes }
|
37
|
+
it_should_behave_like(SimplyAES::Format)
|
38
|
+
end
|
39
|
+
|
40
|
+
describe(SimplyAES::Format::Base64) do
|
41
|
+
let(:formatted) { '/xyu' }
|
42
|
+
let(:bytestring) { "\xFF\x1C\xAE" }
|
43
|
+
let(:short_name) { :base64 }
|
44
|
+
it_should_behave_like(SimplyAES::Format)
|
45
|
+
end
|
46
|
+
|
47
|
+
describe(SimplyAES::Format::Hex) do
|
48
|
+
let(:formatted) { 'ff1cae' }
|
49
|
+
let(:bytestring) { "\xFF\x1C\xAE" }
|
50
|
+
let(:short_name) { :hex }
|
51
|
+
it_should_behave_like(SimplyAES::Format)
|
52
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simply-aes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ryan Biesemeyer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-11-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.6'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.6'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: ruby-appraiser-rubocop
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description:
|
79
|
+
email:
|
80
|
+
- ryan@simplymeasured.com
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .githooks/pre-commit/run-ruby-appraiser
|
86
|
+
- .githooks/pre-commit/run-specs
|
87
|
+
- .gitignore
|
88
|
+
- .rubocop.yml
|
89
|
+
- .travis.yml
|
90
|
+
- CONTRIBUTING.md
|
91
|
+
- Gemfile
|
92
|
+
- LICENSE.md
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- lib/simply-aes.rb
|
96
|
+
- lib/simply-aes/cipher.rb
|
97
|
+
- lib/simply-aes/cipher/error.rb
|
98
|
+
- lib/simply-aes/format.rb
|
99
|
+
- lib/simply-aes/version.rb
|
100
|
+
- simply-aes.gemspec
|
101
|
+
- spec/simply-aes/cipher_spec.rb
|
102
|
+
- spec/simply-aes/format_spec.rb
|
103
|
+
homepage: https://github.com/simplymeasured/simply-aes
|
104
|
+
licenses:
|
105
|
+
- Apache 2
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ! '>='
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 1.8.23
|
125
|
+
signing_key:
|
126
|
+
specification_version: 3
|
127
|
+
summary: Simple AES-256-driven encryption
|
128
|
+
test_files:
|
129
|
+
- spec/simply-aes/cipher_spec.rb
|
130
|
+
- spec/simply-aes/format_spec.rb
|
131
|
+
has_rdoc:
|