signed_json 0.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 +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +31 -0
- data/README.md +82 -0
- data/Rakefile +7 -0
- data/lib/signed_json/errors.rb +7 -0
- data/lib/signed_json/version.rb +3 -0
- data/lib/signed_json.rb +44 -0
- data/signed_json.gemspec +25 -0
- data/spec/signed_json_spec.rb +60 -0
- data/spec/spec_helper.rb +34 -0
- metadata +114 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
signed_json (0.0.1)
|
5
|
+
json
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.1.2)
|
11
|
+
json (1.4.6)
|
12
|
+
rake (0.8.7)
|
13
|
+
rspec (2.0.1)
|
14
|
+
rspec-core (~> 2.0.1)
|
15
|
+
rspec-expectations (~> 2.0.1)
|
16
|
+
rspec-mocks (~> 2.0.1)
|
17
|
+
rspec-core (2.0.1)
|
18
|
+
rspec-expectations (2.0.1)
|
19
|
+
diff-lcs (>= 1.1.2)
|
20
|
+
rspec-mocks (2.0.1)
|
21
|
+
rspec-core (~> 2.0.1)
|
22
|
+
rspec-expectations (~> 2.0.1)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
ruby
|
26
|
+
|
27
|
+
DEPENDENCIES
|
28
|
+
json
|
29
|
+
rake
|
30
|
+
rspec (~> 2.0)
|
31
|
+
signed_json!
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
signed_json
|
2
|
+
============
|
3
|
+
|
4
|
+
Encodes and decodes data to a JSON string signed with OpenSSL HMAC. Great for signed cookies.
|
5
|
+
|
6
|
+
|
7
|
+
Install.
|
8
|
+
--------
|
9
|
+
gem install signed_json
|
10
|
+
|
11
|
+
|
12
|
+
Use.
|
13
|
+
----
|
14
|
+
require 'signed_json'
|
15
|
+
s = SignedJson::Signer.new('your secret')
|
16
|
+
|
17
|
+
### encode ###
|
18
|
+
s.encode 'a string'
|
19
|
+
s.encode ['an', 'array']
|
20
|
+
s.encode { :a => 'hash' }
|
21
|
+
|
22
|
+
### decode ###
|
23
|
+
s.decode '["da7555389d05e04a3367b84aed401cafbbecfe3d","example"]'
|
24
|
+
# => "example"
|
25
|
+
s.decode '["da7555389d05e04a3367b84aed401cafbbecfe3d","tampered"]'
|
26
|
+
# SignedJson::SignatureError
|
27
|
+
|
28
|
+
|
29
|
+
Understand.
|
30
|
+
-----------
|
31
|
+
|
32
|
+
`SignedJson::Signer` takes any JSON encodable data, and returns the data in a JSON string along with an [HMAC][1] signature. The output string can then be decoded back into the original data, with certainty that it was generated using the same secret.
|
33
|
+
|
34
|
+
The signature uses [`OpenSSL::HMAC`][2], with the configurable digest defaulting to SHA1.
|
35
|
+
|
36
|
+
Note that the data is *not* encrypted - it is clearly readable, but altering the data will cause the signature verification to fail. The encoded output looks like this:
|
37
|
+
|
38
|
+
["f47bd6c4108cf503b98b82b2e36ce3e7bae712b5",["an","array","of","strings"]]
|
39
|
+
|
40
|
+
This is ideal for signed cookies, and allows client cookies to be used as a light-weight session store.
|
41
|
+
|
42
|
+
Rails already has a nice signed cookie implementation, but because [`ActiveSupport::MessageVerifier`][3] uses Base64 encoded Marshal.dump instead of JSON, it is barely portable between Ruby versions, let alone different platforms.
|
43
|
+
|
44
|
+
`SignedJson::Signer`, on the other hand, can easily be implemented in other languages, allowing for signed cookies shared between same-domain web applications, for example.
|
45
|
+
|
46
|
+
|
47
|
+
[1]: http://en.wikipedia.org/wiki/HMAC
|
48
|
+
[2]: http://ruby-doc.org/ruby-1.9/classes/OpenSSL/HMAC.html
|
49
|
+
[3]: http://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html
|
50
|
+
|
51
|
+
|
52
|
+
Status.
|
53
|
+
-------
|
54
|
+
|
55
|
+
Ported from my PHP implementation, which is running in high-traffic production environments.
|
56
|
+
|
57
|
+
RSpec speaks for the Ruby implementation:
|
58
|
+
|
59
|
+
$ rake spec
|
60
|
+
|
61
|
+
SignedJson
|
62
|
+
round trip encoding/decoding
|
63
|
+
round-trips a string
|
64
|
+
round-trips an array of strings and ints
|
65
|
+
round-trips a hash with string keys, string and int values
|
66
|
+
round-trips a nested array
|
67
|
+
round-trips a hash/array/string/int structure
|
68
|
+
Signer#encode
|
69
|
+
returns a string
|
70
|
+
returns a valid JSON-encoded array
|
71
|
+
Signer#decode error handling
|
72
|
+
raises SignatureError for incorrect key/signature
|
73
|
+
raises InputError for invalid input
|
74
|
+
|
75
|
+
Finished in 0.0186 seconds
|
76
|
+
9 examples, 0 failures
|
77
|
+
|
78
|
+
|
79
|
+
Meh.
|
80
|
+
----
|
81
|
+
|
82
|
+
(c) 2010 Paul Annesley, MIT license.
|
data/Rakefile
ADDED
data/lib/signed_json.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'signed_json/errors'
|
3
|
+
|
4
|
+
module SignedJson
|
5
|
+
class Signer
|
6
|
+
|
7
|
+
def initialize(secret, digest = 'SHA1')
|
8
|
+
@secret = secret
|
9
|
+
@digest = digest
|
10
|
+
end
|
11
|
+
|
12
|
+
def encode(input)
|
13
|
+
[digest_for(input), input].to_json
|
14
|
+
end
|
15
|
+
|
16
|
+
def decode(input)
|
17
|
+
digest, data = json_decode(input)
|
18
|
+
raise SignatureError unless digest === digest_for(data)
|
19
|
+
data
|
20
|
+
end
|
21
|
+
|
22
|
+
def digest_for(input)
|
23
|
+
# ActiveSupport::MessageVerifier does this, probably for a good reason.
|
24
|
+
require 'openssl' unless defined?(OpenSSL)
|
25
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, input.to_json)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def json_decode(input)
|
31
|
+
begin
|
32
|
+
parts = JSON.parse(input)
|
33
|
+
rescue JSON::ParserError
|
34
|
+
raise InputError
|
35
|
+
end
|
36
|
+
|
37
|
+
raise InputError unless
|
38
|
+
parts.instance_of?(Array) && parts.length == 2
|
39
|
+
|
40
|
+
parts
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/signed_json.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "signed_json/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "signed_json"
|
7
|
+
s.version = SignedJson::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Paul Annesley"]
|
10
|
+
s.email = ["paul@annesley.cc"]
|
11
|
+
s.homepage = "http://github.com/pda/signed_json"
|
12
|
+
s.summary = %q{Encodes and decodes JSON-encodable data into and from a signed JSON string.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "signed_json"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency('json')
|
22
|
+
|
23
|
+
s.add_development_dependency('rspec', ['~> 2.0'])
|
24
|
+
s.add_development_dependency('rake')
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SignedJson do
|
4
|
+
|
5
|
+
describe "round trip encoding/decoding" do
|
6
|
+
|
7
|
+
it "round-trips a string" do
|
8
|
+
"a string".should round_trip_as_signed_json
|
9
|
+
end
|
10
|
+
|
11
|
+
it "round-trips an array of strings and ints" do
|
12
|
+
[1, 'a', 2, 'b'].should round_trip_as_signed_json
|
13
|
+
end
|
14
|
+
|
15
|
+
it "round-trips a hash with string keys, string and int values" do
|
16
|
+
{ 'a' => 'b', 'b' => 2 }.should round_trip_as_signed_json
|
17
|
+
end
|
18
|
+
|
19
|
+
it "round-trips a nested array" do
|
20
|
+
[ 'a', [ 'b', [ 'c', 'd' ], 'e' ], 'f' ].should round_trip_as_signed_json
|
21
|
+
end
|
22
|
+
|
23
|
+
it "round-trips a hash/array/string/int structure" do
|
24
|
+
{ 'a' => [ 'b' ], 'd' => { 'e' => 'f' }, 'g' => 10 }.should round_trip_as_signed_json
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "Signer#encode" do
|
30
|
+
|
31
|
+
it "returns a string" do
|
32
|
+
encoded = SignedJson::Signer.new('right').encode('test')
|
33
|
+
encoded.should be_instance_of(String)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "returns a valid JSON-encoded array" do
|
37
|
+
encoded = SignedJson::Signer.new('right').encode('test')
|
38
|
+
JSON.parse(encoded).should be_instance_of(Array)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "Signer#decode error handling" do
|
44
|
+
|
45
|
+
it "raises SignatureError for incorrect key/signature" do
|
46
|
+
encoded = SignedJson::Signer.new('right').encode('test')
|
47
|
+
lambda {
|
48
|
+
SignedJson::Signer.new('wrong').decode(encoded)
|
49
|
+
}.should raise_error(SignedJson::SignatureError)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "raises InputError for invalid input" do
|
53
|
+
lambda {
|
54
|
+
SignedJson::Signer.new('key').decode('blarg')
|
55
|
+
}.should raise_error(SignedJson::InputError)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'signed_json'
|
2
|
+
|
3
|
+
RSpec::Matchers.define :round_trip_as_signed_json do
|
4
|
+
|
5
|
+
match do |actual|
|
6
|
+
|
7
|
+
signer = SignedJson::Signer.new('some secret')
|
8
|
+
|
9
|
+
@encoded = signer.encode(actual)
|
10
|
+
@decoded = signer.decode(@encoded)
|
11
|
+
|
12
|
+
if @encoded == actual
|
13
|
+
fail_because :not_encoded
|
14
|
+
elsif @decoded != actual
|
15
|
+
fail_because :mismatch
|
16
|
+
else
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def fail_because(reason_code)
|
22
|
+
@reason = reason_code
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
failure_message_for_should do |actual|
|
27
|
+
if @reason == :not_encoded
|
28
|
+
"Expected encoded to be different to original input: #{actual}"
|
29
|
+
elsif @reason == :mismatch
|
30
|
+
"Expected decoded to equal original input, got '#{@decoded}'"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: signed_json
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Paul Annesley
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-11-04 00:00:00 +11:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: json
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rspec
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 2
|
43
|
+
- 0
|
44
|
+
version: "2.0"
|
45
|
+
type: :development
|
46
|
+
version_requirements: *id002
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
prerelease: false
|
50
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id003
|
60
|
+
description:
|
61
|
+
email:
|
62
|
+
- paul@annesley.cc
|
63
|
+
executables: []
|
64
|
+
|
65
|
+
extensions: []
|
66
|
+
|
67
|
+
extra_rdoc_files: []
|
68
|
+
|
69
|
+
files:
|
70
|
+
- .gitignore
|
71
|
+
- Gemfile
|
72
|
+
- Gemfile.lock
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- lib/signed_json.rb
|
76
|
+
- lib/signed_json/errors.rb
|
77
|
+
- lib/signed_json/version.rb
|
78
|
+
- signed_json.gemspec
|
79
|
+
- spec/signed_json_spec.rb
|
80
|
+
- spec/spec_helper.rb
|
81
|
+
has_rdoc: true
|
82
|
+
homepage: http://github.com/pda/signed_json
|
83
|
+
licenses: []
|
84
|
+
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
version: "0"
|
106
|
+
requirements: []
|
107
|
+
|
108
|
+
rubyforge_project: signed_json
|
109
|
+
rubygems_version: 1.3.7
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Encodes and decodes JSON-encodable data into and from a signed JSON string.
|
113
|
+
test_files: []
|
114
|
+
|