nbfritz-encryptedattributes 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/encryptedattributes.rb +169 -0
- data/tests/setup.rb +2 -0
- data/tests/test_encryption.rb +72 -0
- metadata +55 -0
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'digest'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
# This module allows for the easy encryption/decryption of attributes
|
6
|
+
#
|
7
|
+
# Attributes to be encrypted are configured with an attr_encrypted, similar to
|
8
|
+
# an attr_accessor call, at the beginning of the class. Eg:
|
9
|
+
#
|
10
|
+
# class TestClass
|
11
|
+
# attr_encrypted :ssn
|
12
|
+
# ...
|
13
|
+
#
|
14
|
+
# One caveat: at this point, only attributes containing strings can be
|
15
|
+
# encrypted. If you need to encrypt other types, you'll have to handle
|
16
|
+
# the casting to and from String type manually.
|
17
|
+
module EncryptedAttributes
|
18
|
+
module ClassMethods
|
19
|
+
# Class-level writer for the @@encrypted_attributes array
|
20
|
+
def encrypted_attributes=(attributes)
|
21
|
+
@encrypted_attributes = attributes
|
22
|
+
end
|
23
|
+
|
24
|
+
# Class-level reader for the @@encrypted_attributes array
|
25
|
+
def encrypted_attributes
|
26
|
+
@encrypted_attributes
|
27
|
+
end
|
28
|
+
|
29
|
+
# Generates a random IV for seeding a cipher
|
30
|
+
def random_iv
|
31
|
+
OpenSSL::Cipher::Cipher.new('aes-256-cbc').random_iv
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generates a random IV for seeding a cipher
|
35
|
+
def random_key
|
36
|
+
OpenSSL::Cipher::Cipher.new('aes-256-cbc').random_key
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module InstanceMethods
|
41
|
+
attr_writer :cipher_iv
|
42
|
+
attr_accessor :cipher_key, :encrypted
|
43
|
+
|
44
|
+
# reader for @cipher_iv that returns a default value if none exists
|
45
|
+
def cipher_iv
|
46
|
+
@cipher_iv ||= "1234567812345678" # 16 bytes (128 bits) of data
|
47
|
+
end
|
48
|
+
|
49
|
+
# Encrypts all encrypted attributes with the crypt_all private method
|
50
|
+
def encrypt_all; crypt_all(:encrypt); end
|
51
|
+
|
52
|
+
# Decrypts all encrypted attributes with the crypt_all private method
|
53
|
+
def decrypt_all; crypt_all(:decrypt); end
|
54
|
+
|
55
|
+
# Takes the supplied password and optional salt and sets the @cipher_key
|
56
|
+
def key_from_password(password, salt='')
|
57
|
+
@cipher_key = crypto_hash(salt+password)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generates a random 256-bit hash
|
61
|
+
def random_key
|
62
|
+
@cipher_key = self.class.random_key
|
63
|
+
end
|
64
|
+
|
65
|
+
# Generates a random IV
|
66
|
+
def random_iv
|
67
|
+
@cipher_iv = self.class.random_iv
|
68
|
+
end
|
69
|
+
|
70
|
+
# Marshals the @cipher_key and @cipher_iv and returns it in base64
|
71
|
+
def credentials
|
72
|
+
Base64.encode64(Marshal.dump([self.cipher_key, self.cipher_iv]))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Unmarshals supplied base64 data into @cipher_key and @cipher_iv
|
76
|
+
def credentials=(credentials)
|
77
|
+
(@cipher_key, @cipher_iv) = Marshal.load(Base64.decode64(credentials))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Generates a 256-bit hash from the supplied data
|
81
|
+
def crypto_hash(data)
|
82
|
+
Digest::SHA256.digest(data)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a base64 encoded version of crypto_hash
|
86
|
+
def crypto_hash64(data)
|
87
|
+
Base64.encode64(crypto_hash(data))
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
# Encrypts or decrypts all encrypted attributes on this instance
|
92
|
+
#
|
93
|
+
# Mode must be either :encrypt or :decrypt
|
94
|
+
#
|
95
|
+
# Returns false if attempting a decrypt on an already decrypted instance
|
96
|
+
# or an encrypt on an already encrypted instance.
|
97
|
+
def crypt_all(mode = :decrypt)
|
98
|
+
if (mode == :decrypt && @encrypted) || (mode == :encrypt && !@encrypted)
|
99
|
+
self.class.encrypted_attributes.each do |attribute|
|
100
|
+
crypt_attr(attribute.to_s, mode)
|
101
|
+
end
|
102
|
+
@encrypted = (mode == :decrypt) ? false : true
|
103
|
+
return true
|
104
|
+
else
|
105
|
+
return false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Handles the encryption of a single attribute
|
110
|
+
#
|
111
|
+
# If accessors are available, they will be used, otherwise, the instance
|
112
|
+
# variables will be read/written directly.
|
113
|
+
#
|
114
|
+
# Note: will skip encryption/decryption if the attribute is nil
|
115
|
+
def crypt_attr(attribute, mode = :decrypt)
|
116
|
+
method_list = self.methods
|
117
|
+
if method_list.include?(attribute.to_s)
|
118
|
+
data = send(attribute)
|
119
|
+
else
|
120
|
+
data = instance_variable_get("@"+attribute)
|
121
|
+
end
|
122
|
+
|
123
|
+
if data
|
124
|
+
if method_list.include?(writer = attribute.to_s+"=")
|
125
|
+
send(writer, crypt(data, mode))
|
126
|
+
else
|
127
|
+
instance_variable_set("@"+attribute, crypt(data, mode))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Does the real encryption/decryption
|
133
|
+
def crypt(data, mode = :decrypt)
|
134
|
+
data = Base64.decode64(data) if mode == :decrypt
|
135
|
+
|
136
|
+
raise "No key found" unless @cipher_key
|
137
|
+
raise "Only Strings can be encrypted" unless data.class == String
|
138
|
+
cipher = cipher_object(mode)
|
139
|
+
output = cipher.update(data)
|
140
|
+
output << cipher.final
|
141
|
+
|
142
|
+
return (mode == :encrypt) ? Base64.encode64(output) : output
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns a fresh cipher object.
|
146
|
+
#
|
147
|
+
# (developer note: The gotcha here is that the call to cipher to set
|
148
|
+
# the mode to either encrypt or decrypt must preceed the calls to supply
|
149
|
+
# the iv and key!)
|
150
|
+
def cipher_object(mode = :decrypt)
|
151
|
+
cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
152
|
+
cipher.send(mode.to_s)
|
153
|
+
cipher.key = self.cipher_key
|
154
|
+
cipher.iv = self.cipher_iv
|
155
|
+
return cipher
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class Class
|
161
|
+
# Add the attr_encrypted command to Class so new classes can use it
|
162
|
+
def attr_encrypted(*attrs)
|
163
|
+
self.extend(EncryptedAttributes::ClassMethods)
|
164
|
+
send(:encrypted_attributes=, attrs)
|
165
|
+
instance_eval do
|
166
|
+
send(:include, EncryptedAttributes::InstanceMethods)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/tests/setup.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/setup.rb'
|
2
|
+
|
3
|
+
class TestClassAccessors
|
4
|
+
attr_accessor :x, :y
|
5
|
+
attr_encrypted :x, :y
|
6
|
+
end
|
7
|
+
|
8
|
+
class TestRandomEncryption < Test::Unit::TestCase
|
9
|
+
def setup
|
10
|
+
@test_a = TestClassAccessors.new
|
11
|
+
@key = @test_a.random_key
|
12
|
+
@iv = @test_a.random_iv
|
13
|
+
@test_a.x = "test text 1"
|
14
|
+
@test_a.y = "test text 2"
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_random_key
|
18
|
+
assert_equal 32, @key.length
|
19
|
+
assert_instance_of String, @key
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_random_iv
|
23
|
+
assert_equal 16, @iv.length
|
24
|
+
assert_instance_of String, @iv
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_encryption
|
28
|
+
@before = @test_a.x
|
29
|
+
@test_a.encrypt_all
|
30
|
+
@after = @test_a.x
|
31
|
+
|
32
|
+
assert_not_equal @before, @after
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class TestFixedEncryption < Test::Unit::TestCase
|
37
|
+
def setup
|
38
|
+
@test_a = TestClassAccessors.new
|
39
|
+
@key = @test_a.key_from_password('password')
|
40
|
+
@iv = @test_a.cipher_iv = 'a'*16
|
41
|
+
@test_a.x = "test text 1"
|
42
|
+
@test_a.y = "test text 2"
|
43
|
+
|
44
|
+
@credentials = "BAhbByIlXohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtgiFWFhYWFh\n"+
|
45
|
+
"YWFhYWFhYWFhYWE=\n"
|
46
|
+
@encrypted = "LJ/4bevW6+N+9YuthORrqA==\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_fixed_key
|
50
|
+
assert_equal 32, @key.length
|
51
|
+
assert_instance_of String, @key
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_fixed_iv
|
55
|
+
assert_equal 16, @iv.length
|
56
|
+
assert_instance_of String, @iv
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_encryption
|
60
|
+
@before = @test_a.x
|
61
|
+
@test_a.encrypt_all
|
62
|
+
@after = @test_a.x
|
63
|
+
|
64
|
+
assert_not_equal @before, @after
|
65
|
+
assert_equal @after, @encrypted
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_credentials
|
69
|
+
assert_equal 78, @test_a.credentials.size
|
70
|
+
assert_equal @credentials, @test_a.credentials
|
71
|
+
end
|
72
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nbfritz-encryptedattributes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nathan Fritz
|
8
|
+
autorequire: encryptedattributes
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-03 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: This module adds the attr_encrypted call (use it like attr_accessor) to add two-way encryption capabilities to any class being defined.
|
17
|
+
email: fritzn@crown.edu
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- tests/setup.rb
|
26
|
+
- tests/test_encryption.rb
|
27
|
+
- lib/encryptedattributes.rb
|
28
|
+
has_rdoc: true
|
29
|
+
homepage: http://n.thefritzes.net
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: "0"
|
40
|
+
version:
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
requirements: []
|
48
|
+
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 1.0.1
|
51
|
+
signing_key:
|
52
|
+
specification_version: 2
|
53
|
+
summary: Basic module to add symmetric encryption of attributes/instance variables to Ruby classes.
|
54
|
+
test_files:
|
55
|
+
- tests/test_encryption.rb
|