urlcrypt 0.1.2 → 0.2.0
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 +4 -4
- data/README.md +10 -7
- data/Rakefile +2 -3
- data/lib/URLcrypt.rb +17 -18
- data/test/URLcrypt_test.rb +3 -17
- data/test/encryption_test.rb +120 -0
- data/test/regression_test.rb +13 -18
- data/test/test_helper.rb +5 -5
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b88fc478e4e9f1be07a35951eb871bfa2cacc5ca3d6478d95dff1ea24df138d
|
4
|
+
data.tar.gz: 6f01a73f86568c9baec085265539c0b889007ea570273b78609924fc1939a865
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1a7d56dd3bcf4e27a396d3949e9e8cd546cc70a68bff62444ffee6d6e7a8bc1c8370ae94ba28fc24e4d37c01c979cd989d81370854dd7d10aff1489dac39ec7
|
7
|
+
data.tar.gz: a1322a2ebf8c4965c1f6945ba7bf9e61fde1566d5ea33fc05668897a0eb5c3133a83f5dc23ea5d649b7024b06dfec9b8dd0f0f79f04bd8b525919e81be7e636b
|
data/README.md
CHANGED
@@ -20,7 +20,7 @@ that doesn't have other authentication or persistence mechanisms (like cookies):
|
|
20
20
|
* Links that come with an expiration date (à la S3)
|
21
21
|
* Mini-apps that don't persist data on the server
|
22
22
|
|
23
|
-
Works with Ruby 2.
|
23
|
+
Works with Ruby 2.6+
|
24
24
|
|
25
25
|
**Important**: As a general guideline, URL lengths shouldn't exceed about 2000
|
26
26
|
characters in length, as URLs longer than that will not work in some browsers
|
@@ -41,18 +41,21 @@ Add to your Gemfile:
|
|
41
41
|
gem 'urlcrypt', '~> 0.1.1', require: 'URLcrypt'
|
42
42
|
```
|
43
43
|
|
44
|
+
Then, set `ENV['urlcrypt_key']` to the default encryption key you will be using. This should be at least a 256-bit AES key, see below. **To ensure your strings are encoded, URLcrypt uses `ENV.fetch` to check for the variable, so it _must_ be set.**
|
45
|
+
|
44
46
|
## Example
|
45
47
|
|
46
48
|
```ruby
|
47
|
-
# encrypt and
|
48
|
-
# one-time setup, set this to a securely random key with at least 256 bits, see below
|
49
|
-
URLcrypt.key = '...'
|
50
|
-
|
51
|
-
# now encrypt and decrypt!
|
49
|
+
# encrypt and decrypt using the default key from ENV['urlcrypt_key']!
|
52
50
|
URLcrypt.encrypt('chunky bacon!') # => "sgmt40kbmnh1663nvwknxk5l0mZ6Av2ndhgw80rkypnp17xmmg5hy"
|
53
51
|
URLcrypt.decrypt('sgmt40kbmnh1663nvwknxk5l0mZ6Av2ndhgw80rkypnp17xmmg5hy')
|
54
52
|
# => "chunky bacon!"
|
55
53
|
|
54
|
+
# If needed, you can specify a custom key per-call
|
55
|
+
URLcrypt.encrypt('chunky bacon!', key: '...') # => "....."
|
56
|
+
URLcrypt.decrypt('sgmt40kbmnh1663nvwknxk5l0mZ6Av2ndhgw80rkypnp17xmmg5hy', key: '...')
|
57
|
+
# => "chunky bacon!"
|
58
|
+
|
56
59
|
# encoding without encryption (don't use for anything sensitive!), doesn't need key set
|
57
60
|
URLcrypt.encode('chunky bacon!') # => "mnAhk6tlp2qg2yldn8xcc"
|
58
61
|
URLcrypt.decode('mnAhk6tlp2qg2yldn8xcc') # => "chunky bacon!"
|
@@ -70,7 +73,7 @@ ba7f56f8f9873b1653d7f032cc474938fd749ee8fbbf731a7c41d698826aca3cebfffa832be7e6bc
|
|
70
73
|
To use the key with URLcrypt, you'll need to convert that from a hex string into a real byte array:
|
71
74
|
|
72
75
|
```ruby
|
73
|
-
|
76
|
+
ENV['urlcrypt_key'] = ['longhexkeygoeshere'].pack('H*')
|
74
77
|
```
|
75
78
|
|
76
79
|
## Running the Test Suite
|
data/Rakefile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2013 Thomas Fuchs
|
1
|
+
# Copyright (c) 2013-2022 Thomas Fuchs
|
2
2
|
# Copyright (c) 2007-2011 Samuel Tesla
|
3
3
|
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
@@ -33,12 +33,11 @@ gemspec = Gem::Specification.new do |s|
|
|
33
33
|
s.email = "thomas@slash7.com"
|
34
34
|
s.extra_rdoc_files = ["README.md"]
|
35
35
|
s.files = FileList["Rakefile", "{config,lib,test}/**/*"]
|
36
|
-
s.has_rdoc = true
|
37
36
|
s.name = 'urlcrypt'
|
38
37
|
s.require_paths << 'lib'
|
39
38
|
s.requirements << 'none'
|
40
39
|
s.summary = "Securely encode and decode short pieces of arbitrary binary data in URLs."
|
41
|
-
s.version = "0.
|
40
|
+
s.version = "0.2.0"
|
42
41
|
end
|
43
42
|
|
44
43
|
Gem::PackageTask.new(gemspec) do |pkg|
|
data/lib/URLcrypt.rb
CHANGED
@@ -2,14 +2,10 @@ require 'openssl'
|
|
2
2
|
|
3
3
|
module URLcrypt
|
4
4
|
# avoid vowels to not generate four-letter words, etc.
|
5
|
-
# this is important because those words can trigger spam
|
5
|
+
# this is important because those words can trigger spam
|
6
6
|
# filters when URLs are used in emails
|
7
7
|
TABLE = "1bcd2fgh3jklmn4pqrstAvwxyz567890".freeze
|
8
8
|
|
9
|
-
def self.key=(key)
|
10
|
-
@key = key
|
11
|
-
end
|
12
|
-
|
13
9
|
class Chunk
|
14
10
|
def initialize(bytes)
|
15
11
|
@bytes = bytes
|
@@ -31,7 +27,11 @@ module URLcrypt
|
|
31
27
|
[(0..n-1).to_a.reverse.collect {|i| TABLE[(c >> i * 5) & 0x1f].chr},
|
32
28
|
("=" * (8-n))] # TODO: remove '=' padding generation
|
33
29
|
end
|
34
|
-
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.key=(key)
|
33
|
+
warn "`URLcrypt.key=` is deprecated. See the README on using environment variables with URLcrypt."
|
34
|
+
ENV['urlcrypt_key'] = [key].pack('H*')
|
35
35
|
end
|
36
36
|
|
37
37
|
def self.chunks(str, size)
|
@@ -52,27 +52,26 @@ module URLcrypt
|
|
52
52
|
def self.decode(data)
|
53
53
|
chunks(data, 8).collect(&:decode).flatten.join
|
54
54
|
end
|
55
|
-
|
56
|
-
def self.decrypt(data)
|
55
|
+
|
56
|
+
def self.decrypt(data, key: ENV.fetch('urlcrypt_key'))
|
57
57
|
iv, encrypted = data.split('Z').map{|part| decode(part)}
|
58
58
|
fail DecryptError, "not a valid string to decrypt" unless iv && encrypted
|
59
|
-
decrypter = cipher(:decrypt)
|
59
|
+
decrypter = cipher(:decrypt, key: key)
|
60
60
|
decrypter.iv = iv
|
61
|
-
decrypter.update(encrypted) + decrypter.final
|
61
|
+
decrypter.update(encrypted) + decrypter.final
|
62
62
|
end
|
63
|
-
|
64
|
-
def self.encrypt(data)
|
65
|
-
crypter = cipher(:encrypt)
|
63
|
+
|
64
|
+
def self.encrypt(data, key: ENV.fetch('urlcrypt_key'))
|
65
|
+
crypter = cipher(:encrypt, key: key)
|
66
66
|
crypter.iv = iv = crypter.random_iv
|
67
67
|
"#{encode(iv)}Z#{encode(crypter.update(data) + crypter.final)}"
|
68
68
|
end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
def self.cipher(mode)
|
69
|
+
|
70
|
+
private
|
71
|
+
def self.cipher(mode, key:)
|
73
72
|
cipher = OpenSSL::Cipher.new('aes-256-cbc')
|
74
73
|
cipher.send(mode)
|
75
|
-
cipher.key =
|
74
|
+
cipher.key = key.byteslice(0,cipher.key_len)
|
76
75
|
cipher
|
77
76
|
end
|
78
77
|
|
data/test/URLcrypt_test.rb
CHANGED
@@ -23,23 +23,9 @@ class TestURLcrypt < TestClass
|
|
23
23
|
assert_decoding(encoded, original)
|
24
24
|
end
|
25
25
|
end
|
26
|
-
|
27
|
-
def test_encryption
|
28
|
-
# pack() converts this secret into a byte array
|
29
|
-
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d'].pack('H*')
|
30
|
-
URLcrypt::key = secret
|
31
26
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
encrypted = URLcrypt::encrypt(original)
|
36
|
-
assert_equal(URLcrypt::decrypt(encrypted), original)
|
37
|
-
end
|
38
|
-
|
39
|
-
def test_decrypt_error
|
40
|
-
error = assert_raises(URLcrypt::DecryptError) do
|
41
|
-
::URLcrypt::decrypt("just some plaintext")
|
42
|
-
end
|
43
|
-
assert_equal error.message, "not a valid string to decrypt"
|
27
|
+
def test_key_deprecation
|
28
|
+
URLcrypt.key = 'aaaa'
|
29
|
+
assert_equal "\xAA\xAA", ENV.fetch('urlcrypt_key')
|
44
30
|
end
|
45
31
|
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
class URLcryptEncryptionTest < TestClass
|
5
|
+
def teardown
|
6
|
+
ENV["urlcrypt_key"] = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_requires_ENV_if_no_key_provided
|
10
|
+
error = assert_raises(KeyError) do
|
11
|
+
::URLcrypt::decrypt("just some plaintext")
|
12
|
+
end
|
13
|
+
assert_equal error.message, "key not found: \"urlcrypt_key\""
|
14
|
+
|
15
|
+
error = assert_raises(KeyError) do
|
16
|
+
::URLcrypt::encrypt("just some plaintext")
|
17
|
+
end
|
18
|
+
assert_equal error.message, "key not found: \"urlcrypt_key\""
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_encryption_with_ENV_key
|
22
|
+
# pack() converts this secret into a byte array
|
23
|
+
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d'].pack('H*')
|
24
|
+
ENV['urlcrypt_key'] = secret
|
25
|
+
|
26
|
+
assert_equal OpenSSL::Cipher.new('aes-256-cbc').key_len, secret.bytesize
|
27
|
+
|
28
|
+
original = "hello world!"
|
29
|
+
encrypted = URLcrypt::encrypt(original)
|
30
|
+
assert_equal(URLcrypt::decrypt(encrypted), original)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_decrypt_error_with_ENV_key
|
34
|
+
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d'].pack('H*')
|
35
|
+
ENV['urlcrypt_key'] = secret
|
36
|
+
error = assert_raises(URLcrypt::DecryptError) do
|
37
|
+
::URLcrypt::decrypt("just some plaintext")
|
38
|
+
end
|
39
|
+
assert_equal error.message, "not a valid string to decrypt"
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_encryption_with_explicit_key
|
43
|
+
# pack() converts this secret into a byte array
|
44
|
+
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d'].pack('H*')
|
45
|
+
|
46
|
+
assert_equal OpenSSL::Cipher.new('aes-256-cbc').key_len, secret.bytesize
|
47
|
+
|
48
|
+
original = "hello world!"
|
49
|
+
encrypted = URLcrypt::encrypt(original, key: secret)
|
50
|
+
assert_equal(URLcrypt::decrypt(encrypted, key: secret), original)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_decrypt_error_with_explicit_key
|
54
|
+
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d'].pack('H*')
|
55
|
+
error = assert_raises(URLcrypt::DecryptError) do
|
56
|
+
::URLcrypt::decrypt("just some plaintext", key: secret)
|
57
|
+
end
|
58
|
+
assert_equal error.message, "not a valid string to decrypt"
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_threads_with_ENV_keys
|
62
|
+
# pack() converts this secret into a byte array
|
63
|
+
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d'].pack('H*')
|
64
|
+
ENV['urlcrypt_key'] = secret
|
65
|
+
|
66
|
+
assert_equal OpenSSL::Cipher.new('aes-256-cbc').key_len, secret.bytesize
|
67
|
+
|
68
|
+
parent_string = "hello world!"
|
69
|
+
parent_encrypted = URLcrypt::encrypt(parent_string)
|
70
|
+
assert_equal(URLcrypt::decrypt(parent_encrypted), parent_string)
|
71
|
+
|
72
|
+
threads = 100.times.map do |n|
|
73
|
+
Thread.new{
|
74
|
+
original = "Test String #{n}"
|
75
|
+
encrypted = URLcrypt::encrypt(original)
|
76
|
+
assert_equal(URLcrypt::decrypt(encrypted), original)
|
77
|
+
|
78
|
+
thread_encrypted = URLcrypt::encrypt(parent_string)
|
79
|
+
|
80
|
+
assert_equal(URLcrypt::decrypt(thread_encrypted), parent_string)
|
81
|
+
assert_equal(URLcrypt::decrypt(parent_encrypted), parent_string)
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
threads.each { |thr| thr.join }
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_threads_with_explicit_per_thread_keys
|
89
|
+
threads = 100.times.map do |n|
|
90
|
+
Thread.new{
|
91
|
+
key = ["d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d-secret-key-#{n}"].pack('H*')
|
92
|
+
original = "Test String #{n}"
|
93
|
+
encrypted = URLcrypt::encrypt(original, key: key)
|
94
|
+
assert_equal(URLcrypt::decrypt(encrypted, key: key), original)
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
threads.each { |thr| thr.join }
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_threads_with_explicit_per_thread_keys_overriding_ENV_variable
|
102
|
+
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d'].pack('H*')
|
103
|
+
ENV['urlcrypt_key'] = secret
|
104
|
+
|
105
|
+
threads = 100.times.map do |n|
|
106
|
+
Thread.new{
|
107
|
+
key = ["d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d-secret-key-#{n}"].pack('H*')
|
108
|
+
|
109
|
+
original = "Test String #{n}"
|
110
|
+
encrypted = URLcrypt::encrypt(original, key: key)
|
111
|
+
assert_equal(URLcrypt::decrypt(encrypted, key: key), original)
|
112
|
+
|
113
|
+
parent_encryption = URLcrypt::encrypt(original)
|
114
|
+
refute_equal parent_encryption, encrypted
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
threads.each { |thr| thr.join }
|
119
|
+
end
|
120
|
+
end
|
data/test/regression_test.rb
CHANGED
@@ -1,36 +1,31 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
class URLcryptRegressionTest < TestClass
|
3
|
+
def setup
|
4
|
+
# this key was generated via rake secret in a rails app, the pack() converts it into a byte array
|
5
|
+
@secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d721968887a19bdb01af8f59eb5a90d256bd9903355c20b0b4b39bf4048b9b17b'].pack('H*')
|
6
|
+
end
|
7
|
+
|
3
8
|
def test_encryption_and_decryption
|
4
9
|
original = '{"some":"json_data","token":"dfsfsdfsdf"}'
|
5
|
-
encrypted = URLcrypt.encrypt(original)
|
10
|
+
encrypted = URLcrypt.encrypt(original, key: @secret)
|
6
11
|
|
7
|
-
encrypted
|
8
|
-
assert_equal(URLcrypt::decrypt(encrypted), original)
|
12
|
+
assert_equal(URLcrypt::decrypt(encrypted, key: @secret), original)
|
9
13
|
end
|
10
14
|
|
11
15
|
def test_encryption_with_too_long_key
|
12
|
-
|
13
|
-
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d721968887a19bdb01af8f59eb5a90d256bd9903355c20b0b4b39bf4048b9b17b'].pack('H*')
|
14
|
-
URLcrypt::key = secret
|
15
|
-
|
16
|
-
assert OpenSSL::Cipher.new('aes-256-cbc').key_len < secret.bytesize
|
16
|
+
assert OpenSSL::Cipher.new('aes-256-cbc').key_len < @secret.bytesize
|
17
17
|
|
18
18
|
original = "hello world!"
|
19
|
-
encrypted = URLcrypt::encrypt(original)
|
20
|
-
assert_equal(URLcrypt::decrypt(encrypted), original)
|
19
|
+
encrypted = URLcrypt::encrypt(original, key: @secret)
|
20
|
+
assert_equal(URLcrypt::decrypt(encrypted, key: @secret), original)
|
21
21
|
end
|
22
22
|
|
23
23
|
def test_encryption_and_decryption_with_too_long_key
|
24
|
-
|
25
|
-
secret = ['d25883a27b9a639da85ea7e159b661218799c9efa63069fac13a6778c954fb6d721968887a19bdb01af8f59eb5a90d256bd9903355c20b0b4b39bf4048b9b17b'].pack('H*')
|
26
|
-
URLcrypt::key = secret
|
27
|
-
|
28
|
-
assert OpenSSL::Cipher.new('aes-256-cbc').key_len < secret.bytesize
|
24
|
+
assert OpenSSL::Cipher.new('aes-256-cbc').key_len < @secret.bytesize
|
29
25
|
|
30
26
|
original = '{"some":"json_data","token":"dfsfsdfsdf"}'
|
31
|
-
encrypted = URLcrypt.encrypt(original)
|
27
|
+
encrypted = URLcrypt.encrypt(original, key: @secret)
|
32
28
|
|
33
|
-
encrypted
|
34
|
-
assert_equal(URLcrypt::decrypt(encrypted), original)
|
29
|
+
assert_equal(URLcrypt::decrypt(encrypted, key: @secret), original)
|
35
30
|
end
|
36
31
|
end
|
data/test/test_helper.rb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
require 'bundler'
|
3
3
|
Bundler.require(:default, :test)
|
4
4
|
|
5
|
-
require '
|
6
|
-
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start
|
7
7
|
|
8
|
-
require
|
8
|
+
require "minitest/autorun"
|
9
9
|
|
10
|
-
class TestClass < Test
|
10
|
+
class TestClass < Minitest::Test
|
11
11
|
require 'URLcrypt'
|
12
12
|
|
13
13
|
def assert_bytes_equal(string1, string2)
|
@@ -15,7 +15,7 @@ class TestClass < Test::Unit::TestCase
|
|
15
15
|
bytes2 = string2.bytes.to_a.join(':')
|
16
16
|
assert_equal(bytes1, bytes2)
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
def assert_decoding(encoded, plain)
|
20
20
|
decoded = URLcrypt.decode(encoded)
|
21
21
|
assert_bytes_equal(plain, decoded)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: urlcrypt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Fuchs
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: thomas@slash7.com
|
@@ -22,6 +22,7 @@ files:
|
|
22
22
|
- config/environment.rb
|
23
23
|
- lib/URLcrypt.rb
|
24
24
|
- test/URLcrypt_test.rb
|
25
|
+
- test/encryption_test.rb
|
25
26
|
- test/regression_test.rb
|
26
27
|
- test/test_helper.rb
|
27
28
|
homepage:
|
@@ -44,7 +45,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
45
|
version: '0'
|
45
46
|
requirements:
|
46
47
|
- none
|
47
|
-
rubygems_version: 3.1.
|
48
|
+
rubygems_version: 3.1.6
|
48
49
|
signing_key:
|
49
50
|
specification_version: 4
|
50
51
|
summary: Securely encode and decode short pieces of arbitrary binary data in URLs.
|