Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable coder engines #7

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pkg/
coverage
.DS_Store
.ruby-version
.ruby-gemset
12 changes: 12 additions & 0 deletions URLcrypt.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Gem::Specification.new do |s|
s.author = "Thomas Fuchs"
s.email = "[email protected]"
s.extra_rdoc_files = ["README.md"]
s.files = `git ls-files`.split("\n")
s.has_rdoc = true
s.name = 'urlcrypt'
s.require_paths << 'lib'
s.requirements << 'none'
s.summary = "Securely encode and decode short pieces of arbitrary binary data in URLs."
s.version = "0.1.1"
end
140 changes: 109 additions & 31 deletions lib/URLcrypt.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require 'openssl'
require 'base64'
require 'cgi'

module URLcrypt
# avoid vowels to not generate four-letter words, etc.
Expand All @@ -10,6 +12,10 @@ def self.key=(key)
@key = key
end

def self.key
@key
end

class Chunk
def initialize(bytes)
@bytes = bytes
Expand All @@ -29,52 +35,124 @@ def encode
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
("=" * (8-n))] # TODO: remove '=' padding generation
end

end

def self.chunks(str, size)
result = []
bytes = str.bytes
while bytes.any? do
result << Chunk.new(bytes.take(size))
bytes = bytes.drop(size)
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
result
end

# strip '=' padding, because we don't need it
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)
chunks(data, 5).collect(&:encode).flatten.join.tr('=','')
default_coder.new(data: data).encode
end

def self.decode(data)
chunks(data, 8).collect(&:decode).flatten.join
default_coder.new(data: data).decode
end

def self.decrypt(data)
iv, encrypted = data.split('Z').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
default_coder.new(data: data).decrypt
end

def self.encrypt(data)
crypter = cipher(:encrypt)
crypter.iv = iv = crypter.random_iv
"#{encode(iv)}Z#{encode(crypter.update(data) + crypter.final)}"
default_coder.new(data: data).encrypt
end

private

def self.cipher(mode)
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.send(mode)
cipher.key = @key
cipher
end

class DecryptError < ::ArgumentError; end
end
43 changes: 43 additions & 0 deletions test/URLcrypt_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,53 @@ def test_encryption
assert_equal(URLcrypt::decrypt(encrypted), original)
end

def test_base64_encryption
# this key was generated via rake secret in a rails app, the pack() converts it into a byte array
URLcrypt::key =
['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d721968887a19bdb01af8f59eb5a90d256bd9903355c20b0b4b39bf4048b9b17b'].pack('H*')

original = "hello world!"

URLcrypt.default_coder = URLcrypt::Base64Coder

encrypted = URLcrypt::encrypt(original)
assert_equal(URLcrypt::decrypt(encrypted), original)

URLcrypt.default_coder = nil
end

def test_cgi_base64_encryption
# this key was generated via rake secret in a rails app, the pack() converts it into a byte array
URLcrypt::key =
['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d721968887a19bdb01af8f59eb5a90d256bd9903355c20b0b4b39bf4048b9b17b'].pack('H*')

original = "hello world!"

URLcrypt.default_coder = URLcrypt::CGIBase64Coder

encrypted = URLcrypt::encrypt(original)
assert_equal(URLcrypt::decrypt(encrypted), original)

URLcrypt.default_coder = nil
end

def test_decrypt_error
error = assert_raises(URLcrypt::DecryptError) do
::URLcrypt::decrypt("just some plaintext")
end
assert_equal error.message, "not a valid string to decrypt"
end

def test_multiple_coders
coder1 = URLcrypt::BaseCoder.new(key: [SecureRandom.hex(64)].pack("H*"))
coder2 = URLcrypt::BaseCoder.new(key: [SecureRandom.hex(64)].pack("H*"))

str = "hello there friends."
coder1_encrypted = coder1.encrypt(str)
coder2_encrypted = coder2.encrypt(str)

assert_not_equal(coder1_encrypted, coder2_encrypted)
assert_equal(coder1.decrypt(coder1_encrypted), coder2.decrypt(coder2_encrypted))
end

end