encrypted_text 0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+
20
+ # VIM
21
+ *.swp
22
+ *.swo
23
+ *~
24
+
25
+ # OSX
26
+ .DS_Store
27
+ .AppleDouble
28
+ .LSOverride
29
+ Icon
30
+
31
+ # Thumbnails
32
+ ._*
33
+
34
+ # Files that might appear on external disk
35
+ .Spotlight-V100
36
+ .Trashes
data/LICENSE ADDED
@@ -0,0 +1 @@
1
+ Motherflippin' public domain!
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ encrypted_text
2
+ ==============
3
+
4
+ Password-based, two-way encryption with string output. Uses AES encryption
5
+
6
+ Usage example
7
+ -------------
8
+
9
+ In order to encode or decode a message, you should know the **key** and **signature** ahead of time. The key is a 16-, 24-, or 32-character string used for AES encryption key. The signature is prepended to the message before encryption, and verified after decryption.
10
+
11
+ ```ruby
12
+ require 'encrypted_text'
13
+
14
+ codec = EncryptedText.new(
15
+ :signature => '!@#$1234!@#$', # Should not resemble actual message content
16
+ :key => '0123456789ABCDEF' # Should be 16, 24, or 32 chars long
17
+ )
18
+
19
+ encoded = codec.encode("Hello, world!")
20
+ original_message = codec.decode(encoded)
21
+ ```
22
+
23
+ You can also add a random seed, so that repeated encodings of the same message produce different results.
24
+
25
+ ```ruby
26
+ # Continued from previous example
27
+ code.salt_size = 8
28
+ message = "Hello, world!"
29
+
30
+ a = codec.encode(message)
31
+ b = codec.encode(message) # Should be a different result!
32
+ ```
33
+
34
+ Motivation
35
+ ----------
36
+
37
+ I wrote this library so I could generate tokens that encoded actual information, but seemed opaque and pseudo-random to the outside world.
38
+
39
+ In situations where tokens are passed from a service to an outside party and then back again, the service needs some way of resolving tokens passed back to it. Oftentimes this means performing a lookup on a stored mapping (e.g. a database query) between the token and some kind of cleartext data that outside parties never see. But this comes with all the clumsiness of maintaining and interacting with a persistent data store. For some applications, it might be acceptable simply to encode data directly into the token itself, using a secret that only the originating service has access to. EncryptedText provides a simple API to accomplish this.
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
3
+ require 'encrypted_text/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'encrypted_text'
7
+ s.summary = 'Password-based, text-output two-way encryption.'
8
+ s.description = s.summary
9
+ s.homepage = 'http://github.com/jeffomatic/encrypted_text'
10
+ s.version = EncryptedText::VERSION
11
+
12
+ s.authors = [ 'Jeff Lee' ]
13
+ s.email = [ 'jeffomatic@gmail.com' ]
14
+
15
+ s.require_paths = ['lib']
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
19
+
20
+ # Dependencies
21
+
22
+ DEPS = {
23
+ 'fast-aes' => '~> 0.1',
24
+ 'hex_string' => '~> 1.0',
25
+ 'all-your-base' => '~> 0.3',
26
+ }
27
+
28
+ DEPS.each do |lib, version|
29
+ s.add_dependency lib, version
30
+ end
31
+
32
+ s.add_development_dependency('rspec', '~>1.3.1')
33
+ end
@@ -0,0 +1 @@
1
+ require_relative 'encrypted_text/codec'
@@ -0,0 +1,78 @@
1
+ require 'fast-aes'
2
+ require 'hex_string' # Encryption engines typically return binary, so we need to convert to hex
3
+ require 'all_your_base/are/belong_to_us' # Allows us to compress to high-base character strings
4
+
5
+ require_relative 'errs'
6
+
7
+ module EncryptedText
8
+ class Codec
9
+
10
+ attr_accessor :signature
11
+ attr_reader :charset, :key, :salt_size
12
+
13
+ KEY_CHAR_SIZES = [16, 24, 32] # An AES key must be 8-bit char strings of these lengths
14
+ DEFAULT_CHARSET = Array('A'..'Z') + Array('a'..'z') + Array('0'..'9') + [ '-', '_' ] # base 64, URL-safe
15
+
16
+ def initialize(opts)
17
+ config = {
18
+ :charset => DEFAULT_CHARSET,
19
+ :signature => '',
20
+ :key => nil,
21
+ :salt_size => 0,
22
+ }
23
+
24
+ config.keys.each do |k|
25
+ # Replace default value with argument
26
+ v = opts.has_key?(k) ? opts[k] : config[k]
27
+ self.send "#{k}=", v
28
+ end
29
+ end
30
+
31
+ def charset=(charset)
32
+ @charset = charset
33
+ @base_converter = AllYourBase::Are.new(:charset => @charset, :radix => @charset.size)
34
+
35
+ @charset
36
+ end
37
+
38
+ def key=(key)
39
+ @key = key
40
+ @engine = FastAES.new(@key)
41
+
42
+ @key
43
+ end
44
+
45
+ def salt_size=(s)
46
+ raise ArgumentError("Salt size must be integer") unless s.is_a?(Integer)
47
+ @salt_size = s
48
+ end
49
+
50
+ def encode(message)
51
+ salt = (0...@salt_size).map { @charset.sample }.join
52
+ signed = @signature + salt + message
53
+ encrypted = @engine.encrypt(signed)
54
+ hex_string = encrypted.to_hex_string.split(' ').join
55
+ hex = ("1" + hex_string).hex # Add "1" prefix in case hex_string has leading zeroes
56
+ encoded = @base_converter.convert_from_base_10(hex.to_i)
57
+ end
58
+
59
+ def decode(encoded)
60
+ begin
61
+ hex_string = @base_converter.convert_to_base_10(encoded).to_base_16
62
+ hex_string = hex_string[1..-1] if hex_string[0] == '1' # remove "1" prefix
63
+ hex_string = "0" + hex_string if (hex_string.size % 2) != 0 # Make sure we have an even number of hex digits
64
+ byte_string = hex_string.to_byte_string
65
+ decrypted = @engine.decrypt(hex_string.to_byte_string)
66
+ rescue #TODO: don't use generic rescue here
67
+ raise EncryptedText::Err::CannotDecrypt
68
+ end
69
+
70
+ # Ensure that the message is signed correctly
71
+ matches = decrypted.match(/^#{Regexp.escape(signature)}(.+)/)
72
+ raise EncryptedText::Err::BadSignature unless matches
73
+ salted_message = matches[1]
74
+ message = salted_message[@salt_size..-1]
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,12 @@
1
+ module EncryptedText
2
+ module Err
3
+ class Base < RuntimeError
4
+ end
5
+
6
+ class CannotDecrypt < EncryptedText::Err::Base
7
+ end
8
+
9
+ class BadSignature < EncryptedText::Err::Base
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module EncryptedText
2
+ VERSION = '0.1'
3
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+ require 'support/codec_helper'
3
+
4
+ describe EncryptedText::Codec do
5
+
6
+ describe "basic test cases", :basic => true do
7
+
8
+ before(:each) do
9
+ @signature = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
10
+ @key = '0123456789ABCDEF'
11
+ @codec = EncryptedText::Codec.new(:signature => @signature, :key => @key)
12
+ @message = 'Hello, world!'
13
+ end
14
+
15
+ it 'encoded message should not be the same as the message' do
16
+ @codec.encode(@message).should_not == @message
17
+ end
18
+
19
+ it 'encoded message should not be start with the signature' do
20
+ @codec.encode(@message).should_not =~ /^#{@signature}/
21
+ end
22
+
23
+ it 'works with zero-length signatures' do
24
+ @codec.signature = ''
25
+ expect_successful_encode_decode @codec, @message
26
+ end
27
+
28
+ it 'different signatures should produce different encodings' do
29
+ encoded = @codec.encode(@message)
30
+ @codec.signature = rot13(@signature)
31
+ encoded.should_not == @codec.encode(@message)
32
+ end
33
+
34
+ it 'properly encodes a message' do
35
+ expect_successful_encode_decode @codec, @message
36
+ end
37
+
38
+ it 'properly encodes using random salt' do
39
+ @codec.salt_size = 16
40
+ expect_successful_encode_decode @codec, @message
41
+ end
42
+
43
+ it 'produces identical messages without using random salt' do
44
+ @codec.salt_size = 0
45
+ @codec.encode(@message).should == @codec.encode(@message)
46
+ end
47
+
48
+ it 'produces randomized messages using random salt' do
49
+ @codec.salt_size = 16
50
+ @codec.encode(@message).should_not == @codec.encode(@message)
51
+ end
52
+
53
+ it 'properly encodes messages that start with the signature' do
54
+ expect_successful_encode_decode @codec, "#{@signature} #{@message}"
55
+ end
56
+
57
+ it 'raises an error if the key is incorrect' do
58
+ expect_unsuccessful_encode_decode(@codec, @message) do |c, message, encoded|
59
+ c.key = rot13(c.key) # Update the codec with a new key
60
+ encoded # Don't modify the encoded message
61
+ end
62
+ end
63
+
64
+ it 'raises an error if the signatures differ' do
65
+ expect_unsuccessful_encode_decode(@codec, @message) do |c, message, encoded|
66
+ c.signature = rot13(c.signature) # Update the codec with a new signature
67
+ encoded # Don't modify the encoded message
68
+ end
69
+ end
70
+
71
+ it 'raises an error if any character is removed' do
72
+ encoded_message_size = @codec.encode(@message).size
73
+
74
+ (0...encoded_message_size).each do |i|
75
+ expect_unsuccessful_encode_decode(@codec, @message) do |c, message, encoded|
76
+ String(encoded[0...(i - 1)]) + String(encoded[(i + 1)..-1]) # Remove the ith character
77
+ end
78
+ end
79
+ end
80
+
81
+ end # describe "basic test cases"
82
+
83
+ describe "stress tests", :stress => true do
84
+
85
+ it "random" do
86
+ codec = EncryptedText::Codec.new(:signature => random_word, :key => random_key)
87
+
88
+ salt_sizes = [ 0, 2, 4, 8, 16, 32 ]
89
+ signatures = (0...9).map { random_word } + [ '' ]
90
+ keys = (0...10).map { random_key }
91
+ messages = (0...10).map { random_word }
92
+
93
+ salt_sizes.each do |salt_size|
94
+ codec.salt_size = salt_size
95
+ signatures.each do |sig|
96
+ codec.signature = sig
97
+ keys.each do |k|
98
+ codec.key = k
99
+ messages.each do |m|
100
+ expect_successful_encode_decode codec, m
101
+ end # messages.each
102
+ end # keys.each
103
+ end # signatures.each
104
+ end #salt_sizes.each
105
+ end
106
+
107
+ end # describe "stress tests"
108
+
109
+ end
@@ -0,0 +1,10 @@
1
+ $:.unshift(File.expand_path('../lib', File.dirname(__FILE__)))
2
+
3
+ require 'rubygems'
4
+ require 'support/codec_helper'
5
+
6
+ require 'encrypted_text'
7
+
8
+ RSpec.configure do |c|
9
+ c.include CodecHelper
10
+ end
@@ -0,0 +1,28 @@
1
+ module CodecHelper
2
+
3
+ def expect_successful_encode_decode(codec, message)
4
+ encoded = codec.encode(message)
5
+ codec.decode(encoded).should == message
6
+ end
7
+
8
+ def expect_unsuccessful_encode_decode(codec, message, &after_encode)
9
+ encoded = codec.encode(message)
10
+ encoded = after_encode.call(codec, message, encoded) if after_encode
11
+ expect { codec.decode(message) }.to raise_error(EncryptedText::Err::Base)
12
+ end
13
+
14
+ def rot13(string)
15
+ string.tr "A-Za-z", "N-ZA-Mn-za-m"
16
+ end
17
+
18
+ def random_word(opts = {})
19
+ size = opts[:size] || Array(1..100).sample
20
+ charset = Array('a'..'z') + Array('A'..'Z') + Array(0..9)
21
+ Array(0...size).map { charset.sample }.join
22
+ end
23
+
24
+ def random_key
25
+ random_word(:size => EncryptedText::Codec::KEY_CHAR_SIZES.sample)
26
+ end
27
+
28
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: encrypted_text
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeff Lee
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fast-aes
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.1'
30
+ - !ruby/object:Gem::Dependency
31
+ name: hex_string
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: all-your-base
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0.3'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 1.3.1
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 1.3.1
78
+ description: Password-based, text-output two-way encryption.
79
+ email:
80
+ - jeffomatic@gmail.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .gitignore
86
+ - LICENSE
87
+ - README.md
88
+ - encrypted_text.gemspec
89
+ - lib/encrypted_text.rb
90
+ - lib/encrypted_text/codec.rb
91
+ - lib/encrypted_text/errs.rb
92
+ - lib/encrypted_text/version.rb
93
+ - spec/encrypted_text/codec_spec.rb
94
+ - spec/spec_helper.rb
95
+ - spec/support/codec_helper.rb
96
+ homepage: http://github.com/jeffomatic/encrypted_text
97
+ licenses: []
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ! '>='
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubyforge_project:
116
+ rubygems_version: 1.8.21
117
+ signing_key:
118
+ specification_version: 3
119
+ summary: Password-based, text-output two-way encryption.
120
+ test_files: []
121
+ has_rdoc: