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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43b85d8e2005d9c5fb0b2181f9865acb54c85123b9e4aeac7c7c66360aedbdfd
4
- data.tar.gz: '008bc1517bf2fd48531d97bdfa4738520b59c855f145716238bd92edc5ba52bc'
3
+ metadata.gz: 4b88fc478e4e9f1be07a35951eb871bfa2cacc5ca3d6478d95dff1ea24df138d
4
+ data.tar.gz: 6f01a73f86568c9baec085265539c0b889007ea570273b78609924fc1939a865
5
5
  SHA512:
6
- metadata.gz: 9c663737786ae94458f6a28442936fd6bdcdde71bbb198d13c1693d151a028d03d647ff080ccdb2b243fa49310b6cc97b95ea305abc28fd22d46d918f10e00e0
7
- data.tar.gz: 92567f8985d16055720cea95be351e0a675c764734ae9aba366cb85f360d763d623e44136385c09edece332765eb417f5f79be943a2046abcf466323baf6b110
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.1+
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 encode with 256-bit AES
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
- URLcrypt::key = ['longhexkeygoeshere'].pack('H*')
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.1.2"
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 = @key.byteslice(0,cipher.key_len)
74
+ cipher.key = key.byteslice(0,cipher.key_len)
76
75
  cipher
77
76
  end
78
77
 
@@ -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
- assert_equal OpenSSL::Cipher.new('aes-256-cbc').key_len, secret.bytesize
33
-
34
- original = "hello world!"
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
@@ -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 = URLcrypt::encrypt(original)
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
- # this key was generated via rake secret in a rails app, the pack() converts it into a byte array
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
- # this key was generated via rake secret in a rails app, the pack() converts it into a byte array
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 = URLcrypt::encrypt(original)
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 'coveralls'
6
- Coveralls.wear!
5
+ require 'simplecov'
6
+ SimpleCov.start
7
7
 
8
- require 'test/unit'
8
+ require "minitest/autorun"
9
9
 
10
- class TestClass < Test::Unit::TestCase
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.1.2
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: 2020-11-02 00:00:00.000000000 Z
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.2
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.