truby_license 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/truby_license.rb +210 -0
- data/test/test_truby_license.rb +150 -0
- metadata +97 -0
@@ -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
|