encrypted_attributes 0.4.0 → 0.4.1
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/CHANGELOG.rdoc +7 -0
- data/LICENSE +1 -1
- data/Rakefile +18 -17
- data/lib/encrypted_attributes.rb +44 -38
- data/lib/encrypted_attributes/sha_cipher.rb +21 -2
- data/test/app_root/db/migrate/001_create_users.rb +1 -1
- data/test/unit/encrypted_attributes_test.rb +76 -0
- data/test/unit/sha_cipher_test.rb +53 -0
- metadata +13 -20
data/CHANGELOG.rdoc
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
== master
|
2
2
|
|
3
|
+
== 0.4.1 / 2010-03-07
|
4
|
+
|
5
|
+
* Release gems via rake-gemcutter instead of rubyforge
|
6
|
+
* Allow an application-wide default be set for :embed_salt in SHA ciphers
|
7
|
+
* Add support for embedding the salt using the various SHA ciphers
|
8
|
+
* Allow multiple attributes to be encrypted in a single call
|
9
|
+
|
3
10
|
== 0.4.0 / 2009-05-02
|
4
11
|
|
5
12
|
* Replace dynamic :salt option with :embed_salt option and utilizing the #encrypts block
|
data/LICENSE
CHANGED
data/Rakefile
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
1
3
|
require 'rake/testtask'
|
2
4
|
require 'rake/rdoctask'
|
3
5
|
require 'rake/gempackagetask'
|
4
|
-
require 'rake/contrib/sshpublisher'
|
5
6
|
|
6
7
|
spec = Gem::Specification.new do |s|
|
7
8
|
s.name = 'encrypted_attributes'
|
8
|
-
s.version = '0.4.
|
9
|
+
s.version = '0.4.1'
|
9
10
|
s.platform = Gem::Platform::RUBY
|
10
11
|
s.summary = 'Adds support for automatically encrypting ActiveRecord attributes'
|
11
12
|
s.description = s.summary
|
@@ -14,7 +15,7 @@ spec = Gem::Specification.new do |s|
|
|
14
15
|
s.require_path = 'lib'
|
15
16
|
s.has_rdoc = true
|
16
17
|
s.test_files = Dir['test/**/*_test.rb']
|
17
|
-
s.add_dependency 'encrypted_strings', '>= 0.3.
|
18
|
+
s.add_dependency 'encrypted_strings', '>= 0.3.3'
|
18
19
|
|
19
20
|
s.author = 'Aaron Pfeifer'
|
20
21
|
s.email = 'aaron@pluginaweek.org'
|
@@ -51,23 +52,30 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
51
52
|
rdoc.rdoc_dir = 'rdoc'
|
52
53
|
rdoc.title = spec.name
|
53
54
|
rdoc.template = '../rdoc_template.rb'
|
54
|
-
rdoc.options << '--line-numbers'
|
55
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
55
56
|
rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb')
|
56
57
|
end
|
57
|
-
|
58
|
+
|
59
|
+
desc 'Generate a gemspec file.'
|
60
|
+
task :gemspec do
|
61
|
+
File.open("#{spec.name}.gemspec", 'w') do |f|
|
62
|
+
f.write spec.to_ruby
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
58
66
|
Rake::GemPackageTask.new(spec) do |p|
|
59
67
|
p.gem_spec = spec
|
60
|
-
p.need_tar = true
|
61
|
-
p.need_zip = true
|
62
68
|
end
|
63
69
|
|
64
70
|
desc 'Publish the beta gem.'
|
65
71
|
task :pgem => [:package] do
|
72
|
+
require 'rake/contrib/sshpublisher'
|
66
73
|
Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{spec.name}-#{spec.version}.gem").upload
|
67
74
|
end
|
68
75
|
|
69
76
|
desc 'Publish the API documentation.'
|
70
77
|
task :pdoc => [:rdoc] do
|
78
|
+
require 'rake/contrib/sshpublisher'
|
71
79
|
Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{spec.name}", 'rdoc').upload
|
72
80
|
end
|
73
81
|
|
@@ -76,15 +84,8 @@ task :publish => [:pgem, :pdoc, :release]
|
|
76
84
|
|
77
85
|
desc 'Publish the release files to RubyForge.'
|
78
86
|
task :release => [:gem, :package] do
|
79
|
-
require '
|
80
|
-
|
81
|
-
ruby_forge = RubyForge.new.configure
|
82
|
-
ruby_forge.login
|
87
|
+
require 'rake/gemcutter'
|
83
88
|
|
84
|
-
|
85
|
-
|
86
|
-
puts "Releasing #{File.basename(file)}..."
|
87
|
-
|
88
|
-
ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
|
89
|
-
end
|
89
|
+
Rake::Gemcutter::Tasks.new(spec)
|
90
|
+
Rake::Task['gem:push'].invoke
|
90
91
|
end
|
data/lib/encrypted_attributes.rb
CHANGED
@@ -103,45 +103,51 @@ module EncryptedAttributes
|
|
103
103
|
# In the above example, the SHA encryption's <tt>salt</tt> is configured
|
104
104
|
# dynamically based on the user's login and the time at which it was
|
105
105
|
# encrypted. This helps improve the security of the user's password.
|
106
|
-
def encrypts(
|
107
|
-
|
108
|
-
attr_name = attr_name.to_s
|
109
|
-
to_attr_name = (options.delete(:to) || attr_name).to_s
|
106
|
+
def encrypts(*attr_names, &config)
|
107
|
+
base_options = attr_names.last.is_a?(Hash) ? attr_names.pop : {}
|
110
108
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
109
|
+
attr_names.each do |attr_name|
|
110
|
+
options = base_options.dup
|
111
|
+
attr_name = attr_name.to_s
|
112
|
+
to_attr_name = (options.delete(:to) || attr_name).to_s
|
113
|
+
|
114
|
+
# Figure out what cipher is being configured for the attribute
|
115
|
+
mode = options.delete(:mode) || :sha
|
116
|
+
class_name = "#{mode.to_s.classify}Cipher"
|
117
|
+
if EncryptedAttributes.const_defined?(class_name)
|
118
|
+
cipher_class = EncryptedAttributes.const_get(class_name)
|
119
|
+
else
|
120
|
+
cipher_class = EncryptedStrings.const_get(class_name)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Define encryption hooks
|
124
|
+
define_callbacks("before_encrypt_#{attr_name}", "after_encrypt_#{attr_name}")
|
125
|
+
send("before_encrypt_#{attr_name}", options.delete(:before)) if options.include?(:before)
|
126
|
+
send("after_encrypt_#{attr_name}", options.delete(:after)) if options.include?(:after)
|
127
|
+
|
128
|
+
# Set the encrypted value on the configured callback
|
129
|
+
callback = options.delete(:on) || :before_validation
|
130
|
+
|
131
|
+
# Create a callback method to execute on the callback event
|
132
|
+
send(callback, :if => options.delete(:if), :unless => options.delete(:unless)) do |record|
|
133
|
+
record.send(:write_encrypted_attribute, attr_name, to_attr_name, cipher_class, config || options)
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
# Define virtual source attribute
|
138
|
+
if attr_name != to_attr_name && !column_names.include?(attr_name)
|
139
|
+
attr_reader attr_name unless method_defined?(attr_name)
|
140
|
+
attr_writer attr_name unless method_defined?("#{attr_name}=")
|
141
|
+
end
|
142
|
+
|
143
|
+
# Define the reader when reading the encrypted attribute from the database
|
144
|
+
define_method(to_attr_name) do
|
145
|
+
read_encrypted_attribute(to_attr_name, cipher_class, config || options)
|
146
|
+
end
|
147
|
+
|
148
|
+
unless included_modules.include?(EncryptedAttributes::InstanceMethods)
|
149
|
+
include EncryptedAttributes::InstanceMethods
|
150
|
+
end
|
145
151
|
end
|
146
152
|
end
|
147
153
|
end
|
@@ -1,6 +1,24 @@
|
|
1
1
|
module EncryptedAttributes
|
2
2
|
# Adds support for embedding salts in the encrypted value
|
3
3
|
class ShaCipher < EncryptedStrings::ShaCipher
|
4
|
+
class << self
|
5
|
+
# Whether to embed the salt by default
|
6
|
+
attr_accessor :default_embed_salt
|
7
|
+
end
|
8
|
+
|
9
|
+
# Set defaults
|
10
|
+
@default_embed_salt = false
|
11
|
+
|
12
|
+
# Tracks the lengths generated for each hashing algorithm
|
13
|
+
@@algorithm_lengths = {
|
14
|
+
'MD5' => 32,
|
15
|
+
'SHA1' => 40,
|
16
|
+
'SHA2' => 64,
|
17
|
+
'SHA256' => 64,
|
18
|
+
'SHA384' => 96,
|
19
|
+
'SHA512' => 128
|
20
|
+
}
|
21
|
+
|
4
22
|
# Encrypts a string using a Secure Hash Algorithm (SHA), specifically SHA-1.
|
5
23
|
#
|
6
24
|
# Configuration options:
|
@@ -10,9 +28,10 @@ module EncryptedAttributes
|
|
10
28
|
# encrypted value. Default is false. This is useful for storing both
|
11
29
|
# the salt and the encrypted value in the same attribute.
|
12
30
|
def initialize(value, options = {}) #:nodoc:
|
13
|
-
if @embed_salt = options.delete(:embed_salt)
|
31
|
+
if @embed_salt = options.delete(:embed_salt) || self.class.default_embed_salt
|
14
32
|
# The salt is at the end of the value
|
15
|
-
|
33
|
+
algorithm = (options[:algorithm] || EncryptedStrings::ShaCipher.default_algorithm).upcase
|
34
|
+
salt = value[@@algorithm_lengths[algorithm]..-1]
|
16
35
|
options[:salt] = salt unless salt.blank?
|
17
36
|
end
|
18
37
|
|
@@ -61,6 +61,82 @@ class EncryptedAttributesTest < ActiveSupport::TestCase
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
+
class EncryptedAttributesWithMultipleAttributesTest < ActiveSupport::TestCase
|
65
|
+
def setup
|
66
|
+
User.encrypts :password, :password_reminder
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_should_both_using_sha
|
70
|
+
user = create_user(:login => 'admin', :password => 'secret', :password_reminder => 'shhh')
|
71
|
+
assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
|
72
|
+
assert_equal '162cf5debf84cbc2af13da848544c3e2c515b4d3', "#{user.password_reminder}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_should_encrypt_on_invalid_model
|
76
|
+
user = new_user(:login => nil, :password => 'secret', :password_reminder => 'shhh')
|
77
|
+
assert !user.valid?
|
78
|
+
assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
|
79
|
+
assert_equal '162cf5debf84cbc2af13da848544c3e2c515b4d3', "#{user.password_reminder}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_should_not_encrypt_if_attributes_are_nil
|
83
|
+
user = create_user(:login => 'admin', :password => nil, :password_reminder => nil)
|
84
|
+
assert_nil user.password
|
85
|
+
assert_nil user.password_reminder
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_should_not_encrypt_if_attributes_are_blank
|
89
|
+
user = create_user(:login => 'admin', :password => '', :password_reminder => '')
|
90
|
+
assert_equal '', user.password
|
91
|
+
assert_equal '', user.password_reminder
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_should_not_encrypt_any_if_already_encrypted
|
95
|
+
user = create_user(:login => 'admin', :password => 'secret'.encrypt, :password_reminder => 'shhh'.encrypt)
|
96
|
+
assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
|
97
|
+
assert_equal '162cf5debf84cbc2af13da848544c3e2c515b4d3', "#{user.password_reminder}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_should_return_encrypted_attributes_for_saved_record
|
101
|
+
user = create_user(:login => 'admin', :password => 'secret', :password_reminder => 'shhh')
|
102
|
+
user = User.find(user.id)
|
103
|
+
assert user.password.encrypted?
|
104
|
+
assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
|
105
|
+
|
106
|
+
assert user.password_reminder.encrypted?
|
107
|
+
assert_equal '162cf5debf84cbc2af13da848544c3e2c515b4d3', "#{user.password_reminder}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_should_not_encrypt_attributes_if_updating_without_any_changes
|
111
|
+
user = create_user(:login => 'admin', :password => 'secret', :password_reminder => 'shhh')
|
112
|
+
user.login = 'Administrator'
|
113
|
+
user.save!
|
114
|
+
assert user.password.encrypted?
|
115
|
+
assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password}"
|
116
|
+
|
117
|
+
assert user.password_reminder.encrypted?
|
118
|
+
assert_equal '162cf5debf84cbc2af13da848544c3e2c515b4d3', "#{user.password_reminder}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_should_encrypt_attributes_if_updating_with_changes
|
122
|
+
user = create_user(:login => 'admin', :password => 'secret', :password_reminder => 'shhh')
|
123
|
+
user.password = 'shhh'
|
124
|
+
user.password_reminder = 'secret'
|
125
|
+
user.save!
|
126
|
+
assert user.password.encrypted?
|
127
|
+
assert_equal '162cf5debf84cbc2af13da848544c3e2c515b4d3', "#{user.password}"
|
128
|
+
|
129
|
+
assert user.password_reminder.encrypted?
|
130
|
+
assert_equal '8152bc582f58c854f580cb101d3182813dec4afe', "#{user.password_reminder}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def teardown
|
134
|
+
User.class_eval do
|
135
|
+
@before_validation_callbacks = nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
64
140
|
class EncryptedAttributesWithDifferentTargetTest < ActiveSupport::TestCase
|
65
141
|
def setup
|
66
142
|
User.encrypts :password, :to => :crypted_password
|
@@ -14,6 +14,27 @@ class ShaCipherWithoutEmbeddingTest < Test::Unit::TestCase
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
class ShaCipherWithCustomDefaultsTest < Test::Unit::TestCase
|
18
|
+
def setup
|
19
|
+
@original_default_embed_salt = EncryptedAttributes::ShaCipher.default_embed_salt
|
20
|
+
|
21
|
+
EncryptedAttributes::ShaCipher.default_embed_salt = true
|
22
|
+
@cipher = EncryptedAttributes::ShaCipher.new('dc0fc7c07bba982a8d8f18fe138dbea912df5e0ecustom_salt')
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_should_use_remaining_characters_after_password_for_salt
|
26
|
+
assert_equal 'custom_salt', @cipher.salt
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_should_embed_salt_in_encrypted_string
|
30
|
+
assert_equal 'dc0fc7c07bba982a8d8f18fe138dbea912df5e0ecustom_salt', @cipher.encrypt('secret')
|
31
|
+
end
|
32
|
+
|
33
|
+
def teardown
|
34
|
+
EncryptedAttributes::ShaCipher.default_embed_salt = @original_default_embed_salt
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
17
38
|
class ShaCipherWithNoSaltEmbeddedTest < Test::Unit::TestCase
|
18
39
|
def setup
|
19
40
|
@cipher = EncryptedAttributes::ShaCipher.new('dc0fc7c07bba982a8d8f18fe138dbea912df5e0e', :embed_salt => true, :salt => 'custom_salt')
|
@@ -41,3 +62,35 @@ class ShaCipherWithSaltEmbeddedTest < Test::Unit::TestCase
|
|
41
62
|
assert_equal 'dc0fc7c07bba982a8d8f18fe138dbea912df5e0ecustom_salt', @cipher.encrypt('secret')
|
42
63
|
end
|
43
64
|
end
|
65
|
+
|
66
|
+
class ShaCipherWithSaltEmbeddedAndCustomAlgorithmTest < Test::Unit::TestCase
|
67
|
+
def test_should_support_md5
|
68
|
+
cipher = EncryptedAttributes::ShaCipher.new('3b5ba11611dc1ba8bcdb0ff41aa693dcmd5_salt', :algorithm => 'md5', :embed_salt => true)
|
69
|
+
assert_equal 'md5_salt', cipher.salt
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_should_support_sha1
|
73
|
+
cipher = EncryptedAttributes::ShaCipher.new('f8f70e06fed41c86c49766e6963ed4544647d638sha1_salt', :algorithm => 'sha1', :embed_salt => true)
|
74
|
+
assert_equal 'sha1_salt', cipher.salt
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_should_support_sha2
|
78
|
+
cipher = EncryptedAttributes::ShaCipher.new('f2c5bd7ef9317004cca8680e7c12fa8a0b8ea9e7ebab8834ad9d818244d7c1d4sha2_salt', :algorithm => 'sha2', :embed_salt => true)
|
79
|
+
assert_equal 'sha2_salt', cipher.salt
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_should_support_sha256
|
83
|
+
cipher = EncryptedAttributes::ShaCipher.new('1b75f1ebde621856c036118f296bae779548401566c982f2ec1efc089a587689sha256_salt', :algorithm => 'sha256', :embed_salt => true)
|
84
|
+
assert_equal 'sha256_salt', cipher.salt
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_should_support_sha384
|
88
|
+
cipher = EncryptedAttributes::ShaCipher.new('d80570a23a1534d6a32201b01fa84b5d433a275f0aecd9cbdca2c726826e9034f90feb76fcdf49b9eafb21973962c75dsha384_salt', :algorithm => 'sha384', :embed_salt => true)
|
89
|
+
assert_equal 'sha384_salt', cipher.salt
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_should_support_sha512
|
93
|
+
cipher = EncryptedAttributes::ShaCipher.new('44ffca1b3e63f0f1047f42656f43206d7d006c36d44f9f5ffde6c4679dc140f27ff4c8d310bbec902a9231a081e1c9d04236563331df29383e27037bb746df7fsha512_salt', :algorithm => 'sha512', :embed_salt => true)
|
94
|
+
assert_equal 'sha512_salt', cipher.salt
|
95
|
+
end
|
96
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: encrypted_attributes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Pfeifer
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-03-07 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
requirements:
|
21
21
|
- - ">="
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: 0.3.
|
23
|
+
version: 0.3.3
|
24
24
|
version:
|
25
25
|
description: Adds support for automatically encrypting ActiveRecord attributes
|
26
26
|
email: aaron@pluginaweek.org
|
@@ -31,26 +31,17 @@ extensions: []
|
|
31
31
|
extra_rdoc_files: []
|
32
32
|
|
33
33
|
files:
|
34
|
-
- lib/encrypted_attributes.rb
|
35
|
-
- lib/encrypted_attributes
|
36
34
|
- lib/encrypted_attributes/sha_cipher.rb
|
37
|
-
-
|
38
|
-
- test/test_helper.rb
|
39
|
-
- test/unit
|
35
|
+
- lib/encrypted_attributes.rb
|
40
36
|
- test/unit/sha_cipher_test.rb
|
41
37
|
- test/unit/encrypted_attributes_test.rb
|
42
|
-
- test/keys
|
43
|
-
- test/keys/private
|
44
|
-
- test/keys/public
|
45
|
-
- test/app_root
|
46
|
-
- test/app_root/db
|
47
|
-
- test/app_root/db/migrate
|
48
38
|
- test/app_root/db/migrate/001_create_users.rb
|
49
|
-
- test/app_root/config
|
50
|
-
- test/app_root/config/environment.rb
|
51
|
-
- test/app_root/app
|
52
|
-
- test/app_root/app/models
|
53
39
|
- test/app_root/app/models/user.rb
|
40
|
+
- test/app_root/config/environment.rb
|
41
|
+
- test/test_helper.rb
|
42
|
+
- test/factory.rb
|
43
|
+
- test/keys/public
|
44
|
+
- test/keys/private
|
54
45
|
- CHANGELOG.rdoc
|
55
46
|
- init.rb
|
56
47
|
- LICENSE
|
@@ -58,6 +49,8 @@ files:
|
|
58
49
|
- README.rdoc
|
59
50
|
has_rdoc: true
|
60
51
|
homepage: http://www.pluginaweek.org
|
52
|
+
licenses: []
|
53
|
+
|
61
54
|
post_install_message:
|
62
55
|
rdoc_options: []
|
63
56
|
|
@@ -78,9 +71,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
71
|
requirements: []
|
79
72
|
|
80
73
|
rubyforge_project: pluginaweek
|
81
|
-
rubygems_version: 1.3.
|
74
|
+
rubygems_version: 1.3.5
|
82
75
|
signing_key:
|
83
|
-
specification_version:
|
76
|
+
specification_version: 3
|
84
77
|
summary: Adds support for automatically encrypting ActiveRecord attributes
|
85
78
|
test_files:
|
86
79
|
- test/unit/sha_cipher_test.rb
|