truby_license 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,210 @@
1
+ require "base64"
2
+ require "openssl"
3
+ require "zlib"
4
+ require "javabean_xml"
5
+
6
+
7
+ class TrubyLicense
8
+ class TrubyException < Exception ; end
9
+ class PrivateKeyNeeded < TrubyException ; end
10
+ class InvalidLicense < TrubyException ; end
11
+ class InvalidPassword < TrubyException ; end
12
+
13
+ LicenseData = Struct.new :consumerType,
14
+ :notBefore,
15
+ :notAfter,
16
+ :extra,
17
+ :subject,
18
+ :holder,
19
+ :issued,
20
+ :issuer
21
+
22
+ X500Principal = Struct.new :name
23
+
24
+ # Creates an instance able to read and write licenses using the
25
+ # supplied password and key. For writing licenses the key needs to be a
26
+ # private key, otherwise a public key will do.
27
+ def initialize password, key
28
+ raise ArgumentError.new("password must be a String") unless password.is_a? String
29
+ raise ArgumentError.new("key must be an OpenSSL DSA key") unless key.is_a? OpenSSL::PKey::DSA
30
+ @password = password
31
+ @key = key
32
+
33
+ # to hold the ciphers used in the crypt method
34
+ @cipher ||= {}
35
+
36
+ # some extra parameters TrueLicense uses
37
+ @iterations = 2005
38
+ @salt = "\xCE\xFB\xDE\xAC\x05\x02\x19q"
39
+ end
40
+
41
+ def deserialize_license license_blob
42
+ begin
43
+ license_data = gunzip(decrypt(license_blob))
44
+ rescue OpenSSL::Cipher::CipherError
45
+ raise InvalidPassword.new "Could not decrypt license blob"
46
+ rescue Zlib::GzipFile::Error
47
+ raise InvalidLicense.new "Invalid format of license blob: not gzipped corrrectly"
48
+ end
49
+
50
+
51
+ JavabeanXml.from_xml license_data,
52
+
53
+ :string => lambda { |value, properties| value },
54
+ "de.schlichtherle.xml.GenericCertificate" => lambda { |v, properties|
55
+ sig = b64d(properties[:signature])
56
+ license =properties[:encoded]
57
+ algorithm = properties[:signatureAlgorithm]
58
+ encoding = properties[:signatureEncoding]
59
+ unless algorithm == "SHA1withDSA"
60
+ raise NotImplementedError.new(
61
+ "signature algorithm %s has not been implemented".
62
+ % algorithm
63
+ )
64
+ end
65
+ unless encoding == "US-ASCII/Base64"
66
+ raise NotImplementedError.new(
67
+ "signature encoding %s has not been implemented".
68
+ % encoding
69
+ )
70
+ end
71
+ unless verify_signature(license, sig)
72
+ raise InvalidLicense.new("License signature mismatch")
73
+ end
74
+ JavabeanXml.from_xml(license,
75
+ :long => lambda { |value, p| value.to_i },
76
+ :string => lambda { |value, p| value },
77
+ "java.util.Date" => lambda { |value, p| Time.at(value / 1000) },
78
+ "javax.security.auth.x500.X500Principal" => lambda { |value, p| X500Principal.new value },
79
+ "de.schlichtherle.license.LicenseContent" => lambda {|value, properties|
80
+ ld = LicenseData.new
81
+ ld.members.each do |prop|
82
+ ld[prop] = properties[prop.to_sym]
83
+ end
84
+ ld
85
+ }
86
+ )
87
+ }
88
+
89
+ end
90
+
91
+ def serialize_license license_data
92
+ lic_data = license_data.clone # allows us to make changes
93
+ unless @key.private?
94
+ raise PrivateKeyNeeded.new("Cannot use a public key to encrypt the license")
95
+ end
96
+ # make sure issuer and holder are properly wrapped in X500Principal objects
97
+ [:issuer, :holder].each do |prop|
98
+ if lic_data[prop].is_a? String
99
+ lic_data[prop] = X500Principal.new lic_data[prop]
100
+ end
101
+ end
102
+ lic_data.consumerType = lic_data.consumerType.to_s
103
+ inner = JavabeanXml.to_xml lic_data,
104
+ LicenseData => lambda { |value|
105
+ {
106
+ :class => "de.schlichtherle.license.LicenseContent",
107
+ :properties => lic_data.members.inject({}) {|props, prop|
108
+ props[prop.to_sym] = lic_data[prop]
109
+ props
110
+ }
111
+ }
112
+ },
113
+ X500Principal => lambda { |value|
114
+ {
115
+ :class => "javax.security.auth.x500.X500Principal",
116
+ :value => {
117
+ :class => :string,
118
+ :value => value.name
119
+ }
120
+ }
121
+ },
122
+ Time => lambda { |value|
123
+ {
124
+ :class => "java.util.Date",
125
+ :value => {
126
+ :class => :long,
127
+ :value => value.to_i * 1000
128
+ }
129
+ }
130
+ },
131
+ String => lambda { |value|
132
+ {
133
+ :class => :string,
134
+ :value => value
135
+ }
136
+ }
137
+ outer = JavabeanXml.to_xml(
138
+ {
139
+ :class => "de.schlichtherle.xml.GenericCertificate",
140
+ :properties => {
141
+ :encoded => inner,
142
+ :signature => b64e(sign(inner)),
143
+ :signatureAlgorithm => "SHA1withDSA",
144
+ :signatureEncoding => "US-ASCII/Base64"
145
+ }
146
+ },
147
+ String => lambda { |value|
148
+ {
149
+ :class => :string,
150
+ :value => value
151
+ }
152
+ }
153
+ )
154
+ encrypt(gzip(outer))
155
+ end
156
+
157
+
158
+ private
159
+
160
+ def gzip data
161
+ gzipped = ""
162
+ zf = Zlib::GzipWriter.new(StringIO.new(gzipped))
163
+ zf << data
164
+ zf.close
165
+ gzipped
166
+ end
167
+
168
+ def gunzip data
169
+ Zlib::GzipReader.new(StringIO.new(data)).read
170
+ end
171
+
172
+ def sha1 data
173
+ OpenSSL::Digest::SHA1.digest(data)
174
+ end
175
+
176
+ def b64e data
177
+ Base64.encode64 data
178
+ end
179
+
180
+ def b64d string
181
+ Base64.decode64 string
182
+ end
183
+
184
+ def sign data
185
+ @key.syssign sha1(data)
186
+ end
187
+
188
+ def verify_signature data, signature
189
+ @key.sysverify(sha1(data), signature)
190
+ end
191
+
192
+ def crypt mode, data
193
+ unless @cipher[mode]
194
+ @cipher[mode] = OpenSSL::Cipher.new "DES"
195
+ @cipher[mode].send mode
196
+ @cipher[mode].pkcs5_keyivgen @password, @salt, @iterations
197
+ end
198
+ c = @cipher[mode]
199
+ c.update(data) + c.final
200
+ end
201
+
202
+ def decrypt data
203
+ crypt :decrypt, data
204
+ end
205
+
206
+ def encrypt data
207
+ crypt :encrypt, data
208
+ end
209
+
210
+ end
@@ -0,0 +1,150 @@
1
+ require "rubygems"
2
+ require "test/unit"
3
+ require "truby_license"
4
+ class Fixnum
5
+ def seconds
6
+ self
7
+ end
8
+ def minutes
9
+ 60 * seconds
10
+ end
11
+ def hours
12
+ 60 * minutes
13
+ end
14
+ def days
15
+ 24 * hours
16
+ end
17
+ def weeks
18
+ 7 * days
19
+ end
20
+ def months
21
+ 30 * days
22
+ end
23
+ def years
24
+ 365 * days
25
+ end
26
+ def from_now
27
+ Time.at(Time.now.to_i + self)
28
+ end
29
+ def ago
30
+ Time.at(Time.now.to_i - self)
31
+ end
32
+ end
33
+ class TestTrubyLicense < Test::Unit::TestCase
34
+
35
+ def initialize *args
36
+ super *args
37
+ @key1 = { :priv => OpenSSL::PKey::DSA.generate(1024) }
38
+ @key1[:pub] = @key1[:priv].public_key
39
+
40
+ @key2 = { :priv => OpenSSL::PKey::DSA.generate(1024) }
41
+ @key2[:pub] = @key2[:priv].public_key
42
+ end
43
+
44
+ def test_serialize_deserialize
45
+ ld = TrubyLicense::LicenseData.new
46
+
47
+ ld.consumerType = "0"
48
+ ld.notBefore = 5.days.ago
49
+ ld.notAfter = 10.days.from_now
50
+ ld.extra = "an <html><document /></html>"
51
+ ld.subject = "Some subject"
52
+ ld.holder = TrubyLicense::X500Principal.new "CN=Einar Boson"
53
+ ld.issued = Time.at(Time.now.to_i) # strip ns
54
+ ld.issuer = TrubyLicense::X500Principal.new "CN=Einar Boson"
55
+ tl_priv = TrubyLicense.new "my secret password", @key1[:priv]
56
+ tl_pub = TrubyLicense.new "my secret password", @key1[:pub]
57
+
58
+ encoded = tl_priv.serialize_license ld
59
+ decoded = tl_pub.deserialize_license encoded
60
+ ld.each_pair do |prop, val|
61
+ assert_equal val, decoded[prop], "License data should not change through serialization/deserialization"
62
+ end
63
+ end
64
+
65
+ def test_automatic_x500_wrapping
66
+ ld = TrubyLicense::LicenseData.new
67
+
68
+ ld.consumerType = "0"
69
+ ld.notBefore = 5.days.ago
70
+ ld.notAfter = 10.days.from_now
71
+ ld.extra = "an <html><document /></html>"
72
+ ld.subject = "Some subject"
73
+ ld.holder = "CN=Einar Boson1"
74
+ ld.issued = Time.at(Time.now.to_i) # strip ns
75
+ ld.issuer = "CN=Einar Boson2"
76
+
77
+ tl_priv = TrubyLicense.new "my secret password", @key1[:priv]
78
+ tl_pub = TrubyLicense.new "my secret password", @key1[:pub]
79
+
80
+ encoded = tl_priv.serialize_license ld
81
+ decoded = tl_pub.deserialize_license encoded
82
+
83
+ assert decoded.holder.respond_to?(:name) && decoded.holder.name == "CN=Einar Boson1",
84
+ "holder should be automatically wrapped as an X500 object"
85
+ assert decoded.issuer.respond_to?(:name) && decoded.issuer.name == "CN=Einar Boson2",
86
+ "issuer should be automatically wrapped as an X500 object"
87
+
88
+ end
89
+
90
+ def test_exception_on_serializing_With_public_key
91
+ ld = TrubyLicense::LicenseData.new
92
+
93
+ ld.consumerType = "0"
94
+ ld.notBefore = 5.days.ago
95
+ ld.notAfter = 10.days.from_now
96
+ ld.extra = "an <html><document /></html>"
97
+ ld.subject = "Some subject"
98
+ ld.holder = "CN=Einar Boson1"
99
+ ld.issued = Time.at(Time.now.to_i) # strip ns
100
+ ld.issuer = "CN=Einar Boson2"
101
+
102
+ tl = TrubyLicense.new "my secret password", @key1[:pub]
103
+
104
+ assert_raise TrubyLicense::PrivateKeyNeeded do
105
+ tl.serialize_license ld
106
+ end
107
+ end
108
+
109
+ def test_license_invalid_if_signed_with_other_key
110
+ ld = TrubyLicense::LicenseData.new
111
+
112
+ ld.consumerType = "0"
113
+ ld.notBefore = 5.days.ago
114
+ ld.notAfter = 10.days.from_now
115
+ ld.extra = "an <html><document /></html>"
116
+ ld.subject = "Some subject"
117
+ ld.holder = TrubyLicense::X500Principal.new "CN=Einar Boson"
118
+ ld.issued = Time.at(Time.now.to_i) # strip ns
119
+ ld.issuer = TrubyLicense::X500Principal.new "CN=Einar Boson"
120
+ tl_priv = TrubyLicense.new "my secret password", @key1[:priv]
121
+ t2_pub = TrubyLicense.new "my secret password", @key2[:pub]
122
+
123
+ encoded = tl_priv.serialize_license ld
124
+
125
+ assert_raise TrubyLicense::InvalidLicense do
126
+ decoded = t2_pub.deserialize_license encoded
127
+ end
128
+ end
129
+
130
+ def test_license_invalid_if_wrong_password
131
+ ld = TrubyLicense::LicenseData.new
132
+
133
+ ld.consumerType = "0"
134
+ ld.notBefore = 5.days.ago
135
+ ld.notAfter = 10.days.from_now
136
+ ld.extra = "an <html><document /></html>"
137
+ ld.subject = "Some subject"
138
+ ld.holder = TrubyLicense::X500Principal.new "CN=Einar Boson"
139
+ ld.issued = Time.at(Time.now.to_i) # strip ns
140
+ ld.issuer = TrubyLicense::X500Principal.new "CN=Einar Boson"
141
+ tl_priv = TrubyLicense.new "my secret password", @key1[:priv]
142
+ t1_pub = TrubyLicense.new "wrong password", @key1[:pub]
143
+
144
+ encoded = tl_priv.serialize_license ld
145
+
146
+ assert_raise TrubyLicense::InvalidPassword do
147
+ decoded = t1_pub.deserialize_license encoded
148
+ end
149
+ end
150
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: truby_license
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - "Einar Magn\xC3\xBAs Boson"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-12-22 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: nokogiri
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 5
29
+ segments:
30
+ - 1
31
+ - 5
32
+ version: "1.5"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: builder
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 7
44
+ segments:
45
+ - 3
46
+ - 0
47
+ version: "3.0"
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ description: Create and read True License files in ruby
51
+ email: einar.boson@gmail.com
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - lib/truby_license.rb
60
+ - test/test_truby_license.rb
61
+ homepage: http://github.com/einarmagnus/truby_license
62
+ licenses:
63
+ - MIT
64
+ post_install_message:
65
+ rdoc_options: []
66
+
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 57
75
+ segments:
76
+ - 1
77
+ - 8
78
+ - 7
79
+ version: 1.8.7
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ requirements: []
90
+
91
+ rubyforge_project:
92
+ rubygems_version: 1.8.13
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: True License for Ruby
96
+ test_files:
97
+ - test/test_truby_license.rb