ork-encryption 0.0.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.
- checksums.yaml +7 -0
- data/README.md +4 -0
- data/lib/ork/cipher.rb +72 -0
- data/lib/ork/encryption.rb +38 -0
- data/lib/ork/errors.rb +8 -0
- data/lib/ork/serializers/json.rb +59 -0
- data/ork-encryption.gemspec +28 -0
- data/rakefile +10 -0
- data/test/cipher_test.rb +39 -0
- data/test/encrypt_test.rb +115 -0
- data/test/helper.rb +23 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0d98c6c7652d8bf6ceb94c40a8f32d2dce5135fc
|
4
|
+
data.tar.gz: 08b286150cdc8a6582d92fa6357811d643e30b57
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f10fc38e89d28c5a551013e99b3e5ffe04fb63a02a37865ae7e9add307030d871fc82eb15122171da33e9344e83b8153ab4d424da431976969e1c0151b3d7dff
|
7
|
+
data.tar.gz: c1ef9588c03f0abc78498ad8b5d20bb9e428f219ae8510bd9e0212483530b443af2389e832d93e2e3b9fe4429f216185768d98b3f0942ccd2bfff87944999d2c
|
data/README.md
ADDED
data/lib/ork/cipher.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Ork::Encryption
|
4
|
+
|
5
|
+
# Implements a simple object that can either
|
6
|
+
# encrypt or decrypt arbitrary data.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
# cipher = Ork::Encryption::Cipher.new config_hash
|
10
|
+
# cipher.encrypt stuff
|
11
|
+
# cipher.decrypt stuff
|
12
|
+
#
|
13
|
+
class Cipher
|
14
|
+
|
15
|
+
# Creates a cipher that is prepared to encrypt/decrypt a blob.
|
16
|
+
# @param [Hash] config the key/cipher/iv needed to initialize OpenSSL
|
17
|
+
#
|
18
|
+
def initialize(config = {})
|
19
|
+
Cipher.validate_config @config = config
|
20
|
+
@cipher = OpenSSL::Cipher.new @config[:cipher]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Validates the configuration has all the required values to
|
24
|
+
# encrypt and decrypt an object
|
25
|
+
#
|
26
|
+
# Note: if the configuration is invalid, an
|
27
|
+
# `Ork::Encryption::MissingConfig` error is raised.
|
28
|
+
#
|
29
|
+
def self.validate_config(config)
|
30
|
+
if config.nil? || ([:cipher, :key] - config.keys).any?
|
31
|
+
raise MissingConfig,
|
32
|
+
'Make sure to provide the full configuration to Ork::Encryption. ' +
|
33
|
+
'Use Ork::Encryption.init(config_hash) to set the configuration ' +
|
34
|
+
'or assert that Ork::Encryption::Cipher.new receives a non empty' +
|
35
|
+
' hash with :cipher and :key values.'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def random_iv!
|
40
|
+
self.iv = @cipher.random_iv
|
41
|
+
end
|
42
|
+
|
43
|
+
def iv=(iv)
|
44
|
+
@config[:iv] = iv
|
45
|
+
end
|
46
|
+
|
47
|
+
# Encrypt stuff.
|
48
|
+
# @param [Object] blob the data to encrypt
|
49
|
+
def encrypt(blob)
|
50
|
+
initialize_cipher_for :encrypt
|
51
|
+
"#{@cipher.update blob}#{@cipher.final}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Decrypt stuff.
|
55
|
+
# @param [Object] blob the encrypted data to decrypt
|
56
|
+
def decrypt(blob)
|
57
|
+
initialize_cipher_for :decrypt
|
58
|
+
"#{@cipher.update blob}#{@cipher.final}"
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# This sets the mode so OpenSSL knows to encrypt or decrypt, etc.
|
64
|
+
# @param [Symbol] mode either :encrypt or :decrypt
|
65
|
+
def initialize_cipher_for(mode)
|
66
|
+
@cipher.send mode
|
67
|
+
@cipher.key = @config[:key]
|
68
|
+
@cipher.iv = @config[:iv]
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'cipher'
|
2
|
+
require_relative 'errors'
|
3
|
+
require_relative 'serializers/json'
|
4
|
+
|
5
|
+
module Ork
|
6
|
+
module Encryption
|
7
|
+
VERSION = '0.0.1'
|
8
|
+
|
9
|
+
def self.included(klass)
|
10
|
+
raise NotAnOrkDocument unless klass.included_modules.include? Ork::Document
|
11
|
+
klass.content_type Serializers::Json.content_type
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# Initializes the module, setting a configuration and registering
|
16
|
+
# the serializers into Riak API.
|
17
|
+
#
|
18
|
+
def self.init(config)
|
19
|
+
Serializers::Json.register!
|
20
|
+
|
21
|
+
encryption_config config
|
22
|
+
end
|
23
|
+
|
24
|
+
# Accessor for the general Encryptor config.
|
25
|
+
#
|
26
|
+
# config - When nil, it acts like a reader.
|
27
|
+
# When hash, it needs :key and :cipher
|
28
|
+
#
|
29
|
+
# Raises Ork::Encryption::MissingConfig when the config is incomplete.
|
30
|
+
#
|
31
|
+
def self.encryption_config(config = nil)
|
32
|
+
return @encryption_config if config.nil?
|
33
|
+
|
34
|
+
Ork::Encryption::Cipher.validate_config @encryption_config = config
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/ork/errors.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Ork::Encryption
|
2
|
+
module Serializers
|
3
|
+
|
4
|
+
# Implements the {Riak::Serializer} API for the purpose of
|
5
|
+
# encrypting/decrypting Ork documents as JSON.
|
6
|
+
#
|
7
|
+
# @see Encryption
|
8
|
+
class Json
|
9
|
+
|
10
|
+
# The Content-Type of the internal format
|
11
|
+
def self.content_type
|
12
|
+
'application/x-json-encrypted'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Register the serializer into Riak
|
16
|
+
def self.register!
|
17
|
+
Riak::Serializers[content_type] = self
|
18
|
+
end
|
19
|
+
|
20
|
+
# Serializes and encrypts the Ruby hash using the assigned
|
21
|
+
# cipher and Content-Type.
|
22
|
+
#
|
23
|
+
# data - Hash representing persisted_data to serialize/encrypt.
|
24
|
+
#
|
25
|
+
def self.dump(data)
|
26
|
+
json_attributes = data.to_json(Riak.json_options)
|
27
|
+
|
28
|
+
encrypted_object = {
|
29
|
+
iv: Base64.encode64(cipher.random_iv!),
|
30
|
+
data: Base64.encode64(cipher.encrypt json_attributes),
|
31
|
+
version: Ork::Encryption::VERSION
|
32
|
+
}
|
33
|
+
|
34
|
+
encrypted_object.to_json(Riak.json_options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Decrypts and deserializes the blob using the assigned cipher
|
38
|
+
# and Content-Type.
|
39
|
+
#
|
40
|
+
# blob - String of the original content from Riak
|
41
|
+
#
|
42
|
+
def self.load(blob)
|
43
|
+
encrypted_object = Riak::JSON.parse(blob)
|
44
|
+
cipher.iv = Base64.decode64 encrypted_object['iv']
|
45
|
+
decoded_data = Base64.decode64 encrypted_object['data']
|
46
|
+
|
47
|
+
# this serializer now only supports the v2 (0.0.2 - 0.0.4) format
|
48
|
+
Riak::JSON.parse(cipher.decrypt decoded_data)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def self.cipher
|
54
|
+
@cipher ||= Cipher.new(Ork::Encryption.encryption_config)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'ork-encryption'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.date = Time.now.strftime('%Y-%m-%d')
|
5
|
+
s.summary = 'Ruby modeling layer for Riak.'
|
6
|
+
s.summary = 'A simple encryption library for Ork::Documents stored in riak.'
|
7
|
+
s.description = 'Ork is a small Ruby modeling layer for Riak, inspired by Ohm.'
|
8
|
+
s.description = 'Encrypt documents persisted on Riak DB. Inspired in ripple-encryption'
|
9
|
+
s.authors = ['Emiliano Mancuso']
|
10
|
+
s.email = ['emiliano.mancuso@gmail.com']
|
11
|
+
s.homepage = 'http://github.com/emancu/ork-encryption'
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.files = Dir[
|
15
|
+
'README.md',
|
16
|
+
'rakefile',
|
17
|
+
'lib/**/*.rb',
|
18
|
+
'*.gemspec'
|
19
|
+
]
|
20
|
+
s.test_files = Dir['test/*.*']
|
21
|
+
|
22
|
+
s.add_dependency 'riak-client'
|
23
|
+
s.add_dependency 'ork', "~> 0.1.1"
|
24
|
+
s.add_development_dependency 'protest'
|
25
|
+
s.add_development_dependency 'mocha'
|
26
|
+
s.add_development_dependency 'coveralls'
|
27
|
+
end
|
28
|
+
|
data/rakefile
ADDED
data/test/cipher_test.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
Protest.describe 'Ork::Encryption::Cipher' do
|
4
|
+
setup do
|
5
|
+
config = {
|
6
|
+
cipher: 'AES-256-CBC',
|
7
|
+
key: 'fantasticobscurekeygoesherenowty',
|
8
|
+
iv: 'an_iv_really_easy'
|
9
|
+
}
|
10
|
+
|
11
|
+
@cipher = Ork::Encryption::Cipher.new config
|
12
|
+
@text = "This is some nifty text."
|
13
|
+
# this is the example text encrypted (binary string literals use UTF-8 in ruby 2.0
|
14
|
+
@blob = "\xCA\x0F\x80\x9D&)lU\x97h\xC9\xAD\x16+\xBC\xAAQ\xD9\xC7C\x8F\xD7\xEFDoRoS\x0E\xEC\xD3\xA6"
|
15
|
+
@blob = @blob.force_encoding('ASCII-8BIT')
|
16
|
+
end
|
17
|
+
|
18
|
+
test "convert text to an encrypted blob" do
|
19
|
+
assert_equal @blob, @cipher.encrypt(@text), "Encryption failed."
|
20
|
+
end
|
21
|
+
|
22
|
+
test "convert encrypted blob to text" do
|
23
|
+
assert_equal @text, @cipher.decrypt(@blob), "Decryption failed."
|
24
|
+
end
|
25
|
+
|
26
|
+
context "With missing parameter" do
|
27
|
+
test "raise an error if key is missing" do
|
28
|
+
assert_raise Ork::Encryption::MissingConfig do
|
29
|
+
Ork::Encryption::Cipher.new(iv: 'iv', cipher: 'AES-256-CBC')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
test "raise an error if cipher is missing" do
|
34
|
+
assert_raise Ork::Encryption::MissingConfig do
|
35
|
+
Ork::Encryption::Cipher.new(key: 'key')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
require 'mocha/api'
|
3
|
+
# require 'json'
|
4
|
+
|
5
|
+
include(Mocha::API)
|
6
|
+
|
7
|
+
|
8
|
+
class Event
|
9
|
+
include Ork::Document
|
10
|
+
include Ork::Encryption
|
11
|
+
|
12
|
+
attribute :name
|
13
|
+
end
|
14
|
+
|
15
|
+
config_hash = {
|
16
|
+
cipher: 'AES-256-CBC',
|
17
|
+
key: 'fantasticobscurekeygoesherenowty'
|
18
|
+
}
|
19
|
+
|
20
|
+
Protest.describe 'Ork::Encryption' do
|
21
|
+
context 'include Ork::Encryption' do
|
22
|
+
test 'raise an error if it is not a Ork::Document' do
|
23
|
+
assert_raise Ork::Encryption::NotAnOrkDocument do
|
24
|
+
class NotADocument
|
25
|
+
include Ork::Encryption
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'set @content_type' do
|
31
|
+
assert_equal 'application/x-json-encrypted', Event.content_type
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'init' do
|
36
|
+
test 'register the serializers on Riak API' do
|
37
|
+
assert_equal Ork::Encryption::Serializers::Json,
|
38
|
+
Riak::Serializers['application/x-json-encrypted']
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'raise an error if config is invalid' do
|
42
|
+
assert_raise Ork::Encryption::MissingConfig do
|
43
|
+
Ork::Encryption.init(cipher: 'AES-256-CBC')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
test 'encryption_config is set with the same config of Ork::Encryption' do
|
48
|
+
Ork::Encryption.init config_hash
|
49
|
+
|
50
|
+
assert_equal config_hash, Ork::Encryption.encryption_config
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'encrypt / decrypt' do
|
55
|
+
Ork::Encryption.init config_hash
|
56
|
+
|
57
|
+
class Event
|
58
|
+
include Ork::Encryption # reload module
|
59
|
+
end
|
60
|
+
|
61
|
+
setup do
|
62
|
+
@iv = 'an_iv_really_easy'
|
63
|
+
OpenSSL::Cipher.any_instance.stubs(:random_iv).returns(@iv)
|
64
|
+
|
65
|
+
cipher = Ork::Encryption::Cipher.new(config_hash)
|
66
|
+
cipher.random_iv!
|
67
|
+
|
68
|
+
@event = Event.create name: 'Encryption'
|
69
|
+
@attributes = {'name' => 'Encryption', '_type' => 'Event'}
|
70
|
+
@json_attributes = JSON.dump @attributes
|
71
|
+
@encrypted_attributes = cipher.encrypt @json_attributes
|
72
|
+
|
73
|
+
@raw_data = raw_data_from_riak @event
|
74
|
+
@raw_data = JSON.parse @raw_data
|
75
|
+
end
|
76
|
+
|
77
|
+
teardown do
|
78
|
+
OpenSSL::Cipher.any_instance.unstub(:random_iv)
|
79
|
+
end
|
80
|
+
|
81
|
+
test 'saving the object stores attributes encoded in Base64' do
|
82
|
+
assert_equal Base64.encode64(@encrypted_attributes), @raw_data['data']
|
83
|
+
end
|
84
|
+
|
85
|
+
test 'saving the object stores the encrypted attributes' do
|
86
|
+
assert_equal @encrypted_attributes, Base64.decode64(@raw_data['data'])
|
87
|
+
end
|
88
|
+
|
89
|
+
test 'the IV is stored with the encrypted data' do
|
90
|
+
assert_equal Base64.encode64(@iv), @raw_data['iv']
|
91
|
+
end
|
92
|
+
|
93
|
+
test 'the IV is stored with the encrypted data' do
|
94
|
+
assert_equal @iv, Base64.decode64(@raw_data['iv'])
|
95
|
+
end
|
96
|
+
|
97
|
+
test 'the VERSION of Ork::Encryption is stored with the encrypted data' do
|
98
|
+
assert_equal Ork::Encryption::VERSION, @raw_data['version']
|
99
|
+
end
|
100
|
+
|
101
|
+
test 'loading an object decrypts the attributes' do
|
102
|
+
assert_equal({name: 'Encryption'}, Event[@event.id].attributes)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def raw_data_from_riak(object)
|
108
|
+
url = "#{ENV['ORK_RIAK_URL']}" +
|
109
|
+
"/buckets/#{object.class.bucket_name}/keys/#{object.id}"
|
110
|
+
|
111
|
+
`curl -s -XGET #{url}`
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
|
2
|
+
|
3
|
+
require 'coveralls'
|
4
|
+
Coveralls.wear!
|
5
|
+
|
6
|
+
SimpleCov.start do
|
7
|
+
project_name "Ork-Encryptor"
|
8
|
+
command_name "Protest"
|
9
|
+
|
10
|
+
add_filter "/test/"
|
11
|
+
end
|
12
|
+
|
13
|
+
require "rubygems"
|
14
|
+
require "protest"
|
15
|
+
require "ork"
|
16
|
+
require 'ork/encryption'
|
17
|
+
|
18
|
+
Riak.disable_list_keys_warnings = true
|
19
|
+
Protest.report_with(:progress)
|
20
|
+
|
21
|
+
def deny(condition, message="Expected condition to be unsatisfied")
|
22
|
+
assert !condition, message
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ork-encryption
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Emiliano Mancuso
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: riak-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ork
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.1.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.1.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: protest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: coveralls
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Encrypt documents persisted on Riak DB. Inspired in ripple-encryption
|
84
|
+
email:
|
85
|
+
- emiliano.mancuso@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- README.md
|
91
|
+
- rakefile
|
92
|
+
- lib/ork/cipher.rb
|
93
|
+
- lib/ork/encryption.rb
|
94
|
+
- lib/ork/errors.rb
|
95
|
+
- lib/ork/serializers/json.rb
|
96
|
+
- ork-encryption.gemspec
|
97
|
+
- test/cipher_test.rb
|
98
|
+
- test/encrypt_test.rb
|
99
|
+
- test/helper.rb
|
100
|
+
homepage: http://github.com/emancu/ork-encryption
|
101
|
+
licenses:
|
102
|
+
- MIT
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 2.1.9
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: A simple encryption library for Ork::Documents stored in riak.
|
124
|
+
test_files:
|
125
|
+
- test/cipher_test.rb
|
126
|
+
- test/encrypt_test.rb
|
127
|
+
- test/helper.rb
|