simply-aes 0.1.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/.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:
|