crypton 0.1.0.pre1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7d7a624e5d492cfe0734cdef077413c3be95976f
4
+ data.tar.gz: f1c76cefb9da0012925257cc169f6fe2cd5ef592
5
+ SHA512:
6
+ metadata.gz: 8f07fb5c09f7c61459e80e0c62ca8aaed6aa99767cbd286bb00dc7975dd3bb603a079549129f0c6bc26312bcdcee20e605c1879bdbd022991f7e13b1b81511a0
7
+ data.tar.gz: 11287884aa9e5b08310e9d1c53a9b2f241199841398d2aca16239287440f2aba9448f1942f40747db87d04c1b224bf6794f2ea990c1f6e4fcc55d7fae8dd4da8
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,30 @@
1
+ # Crypton
2
+
3
+ Simple crypto.
4
+
5
+ Very alpha. APIs will change.
6
+
7
+ ## Symmetric encryption
8
+
9
+ Uses 256-bit AES in GCM mode. A key of suitable size is derived from the password provided.
10
+
11
+ ```ruby
12
+ encrypted_message = Crypton::Symmetric.encrypt("some secret text", "p4ssw0rd")
13
+ Crypton::Symmetric.decrypt(encrypted_message, "p4ssw0rd") == "some secret text"
14
+ ```
15
+
16
+ ## Encrypted Attributes
17
+
18
+ A helper mixing for transparently encrypting attributes on models.
19
+
20
+ ```ruby
21
+ class User
22
+ include Crypton::EncryptedAttributes
23
+ attr_accessor :encrypted_name
24
+ encrypted_attribute :name, password: ENV["NAME_ENCRYPTION_KEY"]
25
+ end
26
+
27
+ account = BankAccount.new
28
+ account.name = "Ben Hur" # encrypts "Ben Hur" and stores on encrypted_name
29
+ assert account.name == "Ben Hur" # decrypts encrypted_name to retrieve the value
30
+ ```
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'crypton/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "crypton"
8
+ spec.version = Crypton::VERSION
9
+ spec.authors = ["Harry Marr"]
10
+ spec.email = ["engineering@gocardless.com"]
11
+ spec.description = %q{Simple crypto}
12
+ spec.summary = spec.description
13
+ spec.homepage = "https://github.com/gocardless/crypton"
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_development_dependency "rspec", "~> 3.1"
22
+ end
@@ -0,0 +1 @@
1
+ require 'crypton/symmetric'
@@ -0,0 +1,26 @@
1
+ require "crypton/symmetric"
2
+
3
+ module Crypton
4
+ module EncryptedAttributes
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def encrypted_attribute(name, password: nil)
11
+ raise AttributeError, "password must be specified" unless password
12
+ to_field = "encrypted_#{name}"
13
+
14
+ define_method("#{name}=") do |value|
15
+ send("#{to_field}=", Crypton::Symmetric.encrypt(value, password))
16
+ end
17
+
18
+ define_method(name) do
19
+ encrypted_value = send(to_field)
20
+ Crypton::Symmetric.decrypt(encrypted_value, password)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,3 @@
1
+ module Crypton
2
+ class DecryptionError < StandardError; end
3
+ end
@@ -0,0 +1,68 @@
1
+ require "openssl"
2
+ require "base64"
3
+ require "json"
4
+ require "crypton/errors"
5
+
6
+ module Crypton
7
+ module Symmetric
8
+ KEY_SIZE = 256
9
+ ALGORITHM = "aes-#{KEY_SIZE}-gcm"
10
+ KEY_DERIVATION_ROUNDS = 10000
11
+ SCHEME_NAME = "crypton.symmetric"
12
+
13
+ def self.encrypt(data, password)
14
+ salt = OpenSSL::Random.random_bytes(16)
15
+
16
+ cipher = OpenSSL::Cipher.new(ALGORITHM)
17
+ cipher.encrypt
18
+ cipher.key = derive_key(password, salt, KEY_DERIVATION_ROUNDS, KEY_SIZE)
19
+ iv = Base64.strict_encode64(cipher.random_iv)
20
+ cipher.auth_data = "" if cipher.authenticated?
21
+
22
+ cipher_text = cipher.update(data) + cipher.final
23
+
24
+ message = {
25
+ scheme: SCHEME_NAME,
26
+ algo: ALGORITHM,
27
+ key_size: KEY_SIZE,
28
+ kd_rounds: KEY_DERIVATION_ROUNDS,
29
+ salt: Base64.strict_encode64(salt),
30
+ iv: iv,
31
+ cipher_text: Base64.strict_encode64(cipher_text),
32
+ }
33
+ if cipher.authenticated?
34
+ message[:auth_tag] = Base64.strict_encode64(cipher.auth_tag)
35
+ end
36
+
37
+ JSON.generate(message)
38
+ end
39
+
40
+ def self.decrypt(encrypted_data, password)
41
+ message = JSON.parse(encrypted_data, symbolize_names: true)
42
+ key = derive_key(password, Base64.strict_decode64(message[:salt]),
43
+ message[:kd_rounds], message[:key_size])
44
+
45
+ decipher = OpenSSL::Cipher.new(message[:algo])
46
+ decipher.decrypt
47
+ decipher.key = key
48
+ decipher.iv = Base64.strict_decode64(message[:iv])
49
+
50
+ if decipher.authenticated?
51
+ decipher.auth_tag = Base64.strict_decode64(message[:auth_tag])
52
+ decipher.auth_data = ""
53
+ end
54
+
55
+ cipher_text = Base64.strict_decode64(message[:cipher_text])
56
+ decipher.update(cipher_text) + decipher.final
57
+ rescue OpenSSL::Cipher::CipherError => err
58
+ raise DecryptionError, "Error decrypting message: #{err.message}"
59
+ end
60
+
61
+ def self.derive_key(password, salt, rounds, size)
62
+ # Size is provided in bits, then gets converted to bytes for OpenSSL
63
+ OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, rounds, size/8)
64
+ end
65
+
66
+ private_class_method :derive_key
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module Crypton
2
+ VERSION = "0.1.0.pre1".freeze
3
+ end
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+ require "crypton/encrypted_attributes"
3
+
4
+ RSpec.describe Crypton::EncryptedAttributes do
5
+ let(:model) do
6
+ Class.new do
7
+ include Crypton::EncryptedAttributes
8
+ attr_accessor :encrypted_foo
9
+ encrypted_attribute :foo, password: "bar"
10
+ end
11
+ end
12
+
13
+ it "defines a reader method" do
14
+ expect(model.new).to respond_to(:foo)
15
+ end
16
+
17
+ it "defines a writer method" do
18
+ expect(model.new).to respond_to(:foo=)
19
+ end
20
+
21
+ let(:instance) { model.new }
22
+
23
+ describe "reader method" do
24
+ before { allow(Crypton::Symmetric).to receive(:encrypt) { "{...}" } }
25
+
26
+ it "encrypts and stores the value" do
27
+ instance.foo = "baz"
28
+ expect(instance.encrypted_foo).to eq("{...}")
29
+ end
30
+ end
31
+
32
+ describe "writer method" do
33
+ before { allow(Crypton::Symmetric).to receive(:decrypt) { "plaintext" } }
34
+
35
+ it "encrypts and stores the value" do
36
+ instance.encrypted_foo = "{...}"
37
+ expect(instance.foo).to eq("plaintext")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+ require "crypton/symmetric"
3
+
4
+ RSpec.describe Crypton::Symmetric do
5
+ let(:message) { Crypton::Symmetric.encrypt("test", "s3cr3t") }
6
+
7
+ it "works end-to-end" do
8
+ expect(Crypton::Symmetric.decrypt(message, "s3cr3t")).to eq("test")
9
+ end
10
+
11
+ describe ".encrypt" do
12
+ it "generates a JSON-encoded message" do
13
+ expect { JSON.parse(message) }.to_not raise_error
14
+ end
15
+ end
16
+
17
+ describe ".decrypt" do
18
+ context "given an invalid password" do
19
+ it "fails" do
20
+ expect { Crypton::Symmetric.decrypt(message, "wrong password") }.
21
+ to raise_error(Crypton::DecryptionError)
22
+ end
23
+ end
24
+
25
+ context "given an invalid auth tag" do
26
+ let(:tampered_message) do
27
+ JSON.generate(JSON.parse(message).merge("auth_tag" => "0000"))
28
+ end
29
+
30
+ it "fails" do
31
+ expect { Crypton::Symmetric.decrypt(tampered_message, "s3cr3t") }.
32
+ to raise_error(Crypton::DecryptionError)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |config|
2
+ config.disable_monkey_patching!
3
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crypton
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre1
5
+ platform: ruby
6
+ authors:
7
+ - Harry Marr
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ description: Simple crypto
28
+ email:
29
+ - engineering@gocardless.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - .gitignore
35
+ - Gemfile
36
+ - README.md
37
+ - crypton.gemspec
38
+ - lib/crypton.rb
39
+ - lib/crypton/encrypted_attributes.rb
40
+ - lib/crypton/errors.rb
41
+ - lib/crypton/symmetric.rb
42
+ - lib/crypton/version.rb
43
+ - spec/crypton/encrypted_attributes_spec.rb
44
+ - spec/crypton/symmetric_spec.rb
45
+ - spec/spec_helper.rb
46
+ homepage: https://github.com/gocardless/crypton
47
+ licenses:
48
+ - MIT
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '>'
62
+ - !ruby/object:Gem::Version
63
+ version: 1.3.1
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.4.2
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Simple crypto
70
+ test_files:
71
+ - spec/crypton/encrypted_attributes_spec.rb
72
+ - spec/crypton/symmetric_spec.rb
73
+ - spec/spec_helper.rb