encrypted_text 0.1

Sign up to get free protection for your applications and to get access to all the features.
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: