truby_license 0.1.0
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/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
|