forked from stesla/base32
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathURLcrypt.rb
More file actions
158 lines (130 loc) · 3.39 KB
/
URLcrypt.rb
File metadata and controls
158 lines (130 loc) · 3.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
require 'openssl'
require 'base64'
require 'cgi'
module URLcrypt
# avoid vowels to not generate four-letter words, etc.
# this is important because those words can trigger spam
# filters when URLs are used in emails
TABLE = "1bcd2fgh3jklmn4pqrstAvwxyz567890".freeze
def self.key=(key)
@key = key
end
def self.key
@key
end
class Chunk
def initialize(bytes)
@bytes = bytes
end
def decode
bytes = @bytes.take_while {|c| c != 61} # strip padding
bytes = bytes.find_all{|b| !TABLE.index(b.chr).nil? } # remove invalid characters
n = (bytes.length * 5.0 / 8.0).floor
p = bytes.length < 8 ? 5 - (n * 8) % 5 : 0
c = bytes.inject(0) {|m,o| (m << 5) + TABLE.index(o.chr)} >> p
(0..n-1).to_a.reverse.collect {|i| ((c >> i * 8) & 0xff).chr}
end
def encode
n = (@bytes.length * 8.0 / 5.0).ceil
p = n < 8 ? 5 - (@bytes.length * 8) % 5 : 0
c = @bytes.inject(0) {|m,o| (m << 8) + o} << p
[(0..n-1).to_a.reverse.collect {|i| TABLE[(c >> i * 5) & 0x1f].chr},
("=" * (8-n))] # TODO: remove '=' padding generation
end
end
class BaseCoder
def initialize(options = {})
@key = options[:key] || URLcrypt.key
@data = options[:data]
end
# strip '=' padding, because we don't need it
def encode(d = nil)
d ||= @data
chunks(d, 5).collect(&:encode).flatten.join.tr('=','')
end
def decode(d = nil)
d ||= @data
chunks(d, 8).collect(&:decode).flatten.join
end
def encrypt(d = nil)
d ||= @data
crypter = cipher(:encrypt)
crypter.iv = iv = crypter.random_iv
join_parts encode(iv), encode(crypter.update(d) + crypter.final)
end
def split_parts str
str.split('Z')
end
def join_parts *args
args.join("Z")
end
def decrypt(d = nil)
d ||= @data
iv, encrypted = split_parts(d).map{|part| decode(part)}
fail DecryptError, "not a valid string to decrypt" unless iv && encrypted
decrypter = cipher(:decrypt)
decrypter.iv = iv
decrypter.update(encrypted) + decrypter.final
end
def cipher(mode)
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.send(mode)
cipher.key = @key
cipher
end
def chunks(str, size)
result = []
bytes = str.bytes
while bytes.any? do
result << Chunk.new(bytes.take(size))
bytes = bytes.drop(size)
end
result
end
end
class Base64Coder < BaseCoder
def encode(d = nil)
d ||= @data
Base64.urlsafe_encode64(d)
end
def decode(d = nil)
d ||= @data
Base64.urlsafe_decode64(d)
end
def split_parts str
str.split(':')
end
def join_parts *args
args.join(":")
end
end
class CGIBase64Coder < BaseCoder
def encode(d = nil)
d ||= @data
CGI.escape super(d)
end
def decode(d = nil)
d ||= @data
super CGI.unescape(d)
end
end
def self.default_coder
@default_coder || BaseCoder
end
def self.default_coder= val
@default_coder = val
end
def self.encode(data)
default_coder.new(data: data).encode
end
def self.decode(data)
default_coder.new(data: data).decode
end
def self.decrypt(data)
default_coder.new(data: data).decrypt
end
def self.encrypt(data)
default_coder.new(data: data).encrypt
end
class DecryptError < ::ArgumentError; end
end