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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/README.md +30 -0
- data/crypton.gemspec +22 -0
- data/lib/crypton.rb +1 -0
- data/lib/crypton/encrypted_attributes.rb +26 -0
- data/lib/crypton/errors.rb +3 -0
- data/lib/crypton/symmetric.rb +68 -0
- data/lib/crypton/version.rb +3 -0
- data/spec/crypton/encrypted_attributes_spec.rb +40 -0
- data/spec/crypton/symmetric_spec.rb +36 -0
- data/spec/spec_helper.rb +3 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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
|
+
```
|
data/crypton.gemspec
ADDED
@@ -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
|
data/lib/crypton.rb
ADDED
@@ -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,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,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
|
data/spec/spec_helper.rb
ADDED
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
|