voltron-encrypt 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ class CreateVoltronIds < ActiveRecord::Migration
2
+ def change
3
+ create_table :voltron_ids, id: false do |t|
4
+ t.integer :id, limit: 8, primary_key: true
5
+ t.integer :resource_id, limit: 8
6
+ t.string :resource_type
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,91 @@
1
+ module Voltron
2
+ class Encrypt
3
+ module Generators
4
+ class InstallGenerator < Rails::Generators::Base
5
+
6
+ source_root File.expand_path("../../../templates", __FILE__)
7
+
8
+ desc "Add Voltron Encrypt initializer"
9
+
10
+ def inject_initializer
11
+
12
+ voltron_initialzer_path = Rails.root.join("config", "initializers", "voltron.rb")
13
+
14
+ unless File.exist? voltron_initialzer_path
15
+ unless system("cd #{Rails.root.to_s} && rails generate voltron:install")
16
+ puts "Voltron initializer does not exist. Please ensure you have the 'voltron' gem installed and run `rails g voltron:install` to create it"
17
+ return false
18
+ end
19
+ end
20
+
21
+ current_initiailzer = File.read voltron_initialzer_path
22
+
23
+ unless current_initiailzer.match(Regexp.new(/^# === Voltron Encrypt Configuration ===/))
24
+ inject_into_file(voltron_initialzer_path, after: "Voltron.setup do |config|\n") do
25
+ <<-CONTENT
26
+
27
+ # === Voltron Encrypt Configuration ===
28
+
29
+ # The offset used in generating base64 encoded ids. A higher number means larger ids (i.e. - "7feIds" instead of "6f"),
30
+ # but can potentially produce large base64 encoded ids
31
+ # DON'T change this number once records whose id's are being encoded exist in the database
32
+ # as all decoded ids will be incorrect
33
+ # config.encrypt.offset = 262144
34
+
35
+ # The location of the blacklist, words that should NOT be permitted in the form of generated ids
36
+ # Each word should be on it's own line, and only contain [A-Z], no spaces, dashes, underscores, or numbers
37
+ # Each word is automatically matched against it's literal, case-insensitive, and l33t spellings, with dashes
38
+ # and underscores optionally preceding/following each character.
39
+ # i.e. - the blacklist word "toke" will match [toke, tOKE, 7oke, t0k3, t-o-k-e, -t0--k3--, etc...]
40
+ # config.encrypt.blacklist = Rails.root.join("config", "locales", "blacklist.txt")
41
+
42
+ # The seed used to randomize base 64 characters. Once set, it should NOT EVER be changed.
43
+ # Doing so will result in incorrect decoded ids, followed by large crowds with pitchforks and torches
44
+ # Running `rake secret` is a good way to generate a random seed for this config value
45
+ # config.encrypt.seed = ""
46
+ CONTENT
47
+ end
48
+ end
49
+ end
50
+
51
+ def copy_blacklist
52
+ unless File.exist? Rails.root.join("config", "locales", "blacklist.txt")
53
+ copy_file "config/locales/blacklist.txt", Rails.root.join("config", "locales", "blacklist.txt")
54
+ end
55
+ end
56
+
57
+ def copy_migrations
58
+ copy_migration "create_voltron_ids"
59
+ end
60
+
61
+ protected
62
+
63
+ def copy_migration(filename)
64
+ if migration_exists?(Rails.root.join("db", "migrate"), filename)
65
+ say_status("skipped", "Migration #{filename}.rb already exists")
66
+ else
67
+ copy_file "db/migrate/#{filename}.rb", Rails.root.join("db", "migrate", "#{migration_number}_#{filename}.rb")
68
+ end
69
+ end
70
+
71
+ def migration_exists?(dirname, filename)
72
+ Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{filename}.rb$/).first
73
+ end
74
+
75
+ def migration_id_exists?(dirname, id)
76
+ Dir.glob("#{dirname}/#{id}*").length > 0
77
+ end
78
+
79
+ def migration_number
80
+ @migration_number ||= Time.now.strftime("%Y%m%d%H%M%S").to_i
81
+
82
+ while migration_id_exists?(Rails.root.join("db", "migrate"), @migration_number) do
83
+ @migration_number += 1
84
+ end
85
+
86
+ @migration_number
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,19 @@
1
+ module Voltron
2
+ class Config
3
+
4
+ def encrypt
5
+ @encrypt ||= Encrypt.new
6
+ end
7
+
8
+ class Encrypt
9
+
10
+ attr_accessor :offset, :seed, :blacklist
11
+
12
+ def initialize
13
+ @offset ||= 262144
14
+ @seed ||= ""
15
+ @blacklist ||= Rails.root.join("config", "locales", "blacklist.txt")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Voltron
2
+ class Encrypt
3
+ class Engine < Rails::Engine
4
+
5
+ isolate_namespace Voltron
6
+
7
+ initializer "voltron.encrypt.initialize" do
8
+ ::ActiveRecord::Base.send :extend, ::Voltron::Encryptable
9
+
10
+ # Corrects sidekiq resource lookup by forcing it to find_by_id rather than find with the model id
11
+ # We either want it to find with to_param value, or find_by_id with the actual id, this is the latter
12
+ ::GlobalID::Locator.use Rails.application.railtie_name.remove("_application").dasherize do |gid|
13
+ gid.model_class.find_by_id gid.model_id
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ module Voltron
2
+ class Encrypt
3
+ VERSION = "0.1.2".freeze
4
+ end
5
+ end
@@ -0,0 +1,83 @@
1
+ require "voltron"
2
+ require "voltron/encrypt/version"
3
+ require "voltron/config/encrypt"
4
+ require "voltron/encryptable"
5
+
6
+ module Voltron
7
+ class Encrypt
8
+
9
+ def encode(input)
10
+ radix = digits.length
11
+ i = input.to_i + Voltron.config.encrypt.offset.to_i # Increase the number just so we don't end up with id's like "E" or "d3" on low number ids
12
+
13
+ raise ArgumentError.new("Value #{val} cannot be less than zero") if i < 0
14
+
15
+ out = []
16
+ begin
17
+ rem = i % radix
18
+ i /= radix
19
+ out << digits[rem]
20
+ end until i == 0
21
+
22
+ out.reverse.join
23
+ end
24
+
25
+ def decode(input)
26
+ inp = input.to_s.split("")
27
+ out = 0
28
+
29
+ begin
30
+ chr = inp.shift
31
+ out += (digits.length**inp.length)*digits.index(chr)
32
+ end until inp.empty?
33
+
34
+ out - Voltron.config.encrypt.offset.to_i # Decrease the number by the same offset amount
35
+ end
36
+
37
+ def blacklisted?(input)
38
+ encoded = encode(input)
39
+
40
+ pattern = ["\\b([_\\-])*"]
41
+ encoded.split("").each do |c|
42
+ subs = translations[c.downcase] || []
43
+ c = "\\#{c}" if c == "-"
44
+ pattern << "[#{c}#{subs.join}]([_\\-])*"
45
+ end
46
+ pattern << "\\b"
47
+
48
+ regex = Regexp.new(pattern.join, Regexp::IGNORECASE)
49
+ !blacklist(encoded.length).match(regex).nil?
50
+ end
51
+
52
+ private
53
+
54
+ def blacklist(len = 6)
55
+ if File.exist?(Voltron.config.encrypt.blacklist.to_s)
56
+ File.readlines(Voltron.config.encrypt.blacklist).map(&:strip).reject { |line| line.length > len }.join(" ")
57
+ else
58
+ ""
59
+ end
60
+ end
61
+
62
+ def translations
63
+ {
64
+ "a" => ["4"],
65
+ "e" => ["3"],
66
+ "i" => ["1", "l"],
67
+ "o" => ["0"],
68
+ "s" => ["5"],
69
+ "t" => ["7"],
70
+ "b" => ["8"],
71
+ "z" => ["2"],
72
+ "g" => ["9"]
73
+ }
74
+ end
75
+
76
+ def digits
77
+ rnd = Random.new(Voltron.config.encrypt.seed.to_s.to_i(24))
78
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".split("").shuffle(random: rnd)
79
+ end
80
+ end
81
+ end
82
+
83
+ require "voltron/encrypt/engine" if defined?(Rails)
@@ -0,0 +1,125 @@
1
+ module Voltron
2
+ module Encryptable
3
+
4
+ def encrypted_id
5
+ extend ClassMethods
6
+ include InstanceMethods
7
+
8
+ has_one :encryptable, as: :resource, class_name: "Voltron::Id"
9
+
10
+ before_create do
11
+ self.build_encryptable id: find_id
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def find(*args)
17
+ scope = args.slice!(0)
18
+ options = args.slice!(0) || {}
19
+
20
+ if !options[:bypass] && ![:first, :last, :all].include?(scope.try(:to_sym))
21
+ scope = decoded_ids(scope)
22
+ end
23
+
24
+ super(scope)
25
+ end
26
+
27
+ def exists?(conditions = :none)
28
+ if conditions.is_a?(String)
29
+ # If conditions is a string, assume it's an encoded id
30
+ super(decoded_ids(conditions))
31
+ else
32
+ # Otherwise do what exists? normally does
33
+ super(conditions)
34
+ end
35
+ end
36
+
37
+ def destroy(id)
38
+ super(decoded_ids(id))
39
+ end
40
+
41
+ def delete(id)
42
+ super(decoded_ids(id))
43
+ end
44
+
45
+ private
46
+
47
+ def decoded_ids(*ids)
48
+ crypt = Voltron::Encrypt.new
49
+
50
+ ids.flatten!
51
+ ids.map! { |id| crypt.decode(id).to_i }
52
+ ids.reject! { |id| id > 9223372036854775807 } # Remove id if given decoded value is greater than max PostgreSQL value
53
+ ids = Voltron::Id.where(id: ids).pluck(:resource_id)
54
+ ids = ids.first if ids.length == 1
55
+ ids
56
+ end
57
+ end
58
+
59
+ module InstanceMethods
60
+ def to_param
61
+ return super if encryptable.nil?
62
+
63
+ crypt = Voltron::Encrypt.new
64
+ crypt.encode(encryptable.id)
65
+ end
66
+
67
+ def find_id
68
+ # Helps determine the min/max value. For example if amount is 1100, min will be 1024, if amount is 947, min will be 1
69
+ amount = Voltron::Id.count.to_f
70
+
71
+ # The range of ids from which we try to find an unused id, i.e. - (1..1024), (1025..2048), (2049..3073)
72
+ factor = 1024.to_f
73
+
74
+ # Get the min and max value of the range segment
75
+ min = (((amount/factor).floor*factor) + 1).to_i
76
+ max = (((amount+1)/factor).ceil*factor).to_i
77
+
78
+ # Get all ids in the determined range segment, these are the ids we cannot choose from
79
+ used = Voltron::Id.where(id: min..max).pluck(:id)
80
+ # Get the candidates, an array of values from min to max, subtracting the used values from above
81
+ candidates = ((min..max).to_a - used).shuffle
82
+ # Get the first candidate, the first value off the shuffled array
83
+ candidate = candidates.shift
84
+
85
+ # Check for blacklisted words given the candidate id
86
+ while crypt.blacklisted?(candidate) do
87
+ # If id chosen is a blacklisted word, insert it as a placeholder record and try again
88
+ Voltron::Id.create(id: candidate, resource_id: 0, resource_type: :blacklist)
89
+
90
+ # If no more candidates to choose from, re-run find_id, it will get a new set of candidates from the next segment
91
+ return find_id if candidates.empty?
92
+
93
+ # Pick the next candidate
94
+ candidate = candidates.shift
95
+ end
96
+
97
+ # The id chosen is good, not a blacklisted word. Use it
98
+ candidate
99
+ end
100
+
101
+ def reload(options = nil)
102
+ clear_aggregation_cache
103
+ clear_association_cache
104
+ self.class.connection.clear_query_cache
105
+
106
+ fresh_object =
107
+ if options && options[:lock]
108
+ self.class.unscoped { self.class.lock(options[:lock]).find_by(id: encryptable.resource_id) }
109
+ else
110
+ self.class.unscoped { self.class.find(encryptable.resource_id, bypass: true) }
111
+ end
112
+
113
+ @attributes = fresh_object.instance_variable_get("@attributes")
114
+ @new_record = false
115
+ self
116
+ end
117
+
118
+ private
119
+
120
+ def crypt
121
+ @crypt ||= Voltron::Encrypt.new
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'voltron/encrypt/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "voltron-encrypt"
8
+ spec.version = Voltron::Encrypt::VERSION
9
+ spec.authors = ["Eric Hainer"]
10
+ spec.email = ["eric@commercekitchen.com"]
11
+
12
+ spec.summary = %q{Enables base 64 encoded ids on rails models}
13
+ spec.homepage = "https://github.com/ehainer/voltron-encrypt"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "rails", ">= 4.2"
22
+ spec.add_dependency "voltron", ">= 0.1.0"
23
+
24
+ spec.add_development_dependency "bundler", ">= 1.12"
25
+ spec.add_development_dependency "rake", ">= 10.0"
26
+ spec.add_development_dependency "rspec", ">= 3.0"
27
+ spec.add_development_dependency "rspec-rails", ">= 3.4"
28
+ spec.add_development_dependency "sqlite3", ">= 1.2"
29
+ spec.add_development_dependency "simplecov", "0.11.0"
30
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: voltron-encrypt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Eric Hainer
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-10-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: voltron
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.0
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.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3.4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '1.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '1.2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 0.11.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '='
123
+ - !ruby/object:Gem::Version
124
+ version: 0.11.0
125
+ description:
126
+ email:
127
+ - eric@commercekitchen.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rspec"
134
+ - ".travis.yml"
135
+ - CODE_OF_CONDUCT.md
136
+ - Gemfile
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - app/models/voltron/id.rb
141
+ - bin/console
142
+ - bin/setup
143
+ - lib/generators/templates/config/locales/blacklist.txt
144
+ - lib/generators/templates/db/migrate/create_voltron_ids.rb
145
+ - lib/generators/voltron/encrypt/install_generator.rb
146
+ - lib/voltron/config/encrypt.rb
147
+ - lib/voltron/encrypt.rb
148
+ - lib/voltron/encrypt/engine.rb
149
+ - lib/voltron/encrypt/version.rb
150
+ - lib/voltron/encryptable.rb
151
+ - voltron-encrypt.gemspec
152
+ homepage: https://github.com/ehainer/voltron-encrypt
153
+ licenses:
154
+ - MIT
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubyforge_project:
172
+ rubygems_version: 2.4.8
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Enables base 64 encoded ids on rails models
176
+ test_files: []