information_card 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.
@@ -0,0 +1,2 @@
1
+ * 0.1.0
2
+ - Initial release.
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2007, ThoughtWorks
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of ThoughtWorks nor the
15
+ names of its contributors may be used to endorse or promote products
16
+ derived from this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README ADDED
@@ -0,0 +1,45 @@
1
+ =Information Card
2
+
3
+ A Ruby library for processing information cards.
4
+
5
+ ==Features
6
+ * Easy to use API for decrypting, validating and processing SAML formatted information cards
7
+ * Basic XML canonicalization (not yet fully c14n compliant)
8
+ * Flexible configuration
9
+ * Comprehensive test suite
10
+
11
+ ==Installing
12
+ gem intall information_card
13
+
14
+ The library is known to work with Ruby 1.8.4 and up on Win32, Max OSX and Ubuntu 6.06
15
+ Examples were tested with Rails 1.2.3
16
+
17
+ ==Getting Started
18
+ For documentation, visit http://informationcardruby.com/documents
19
+
20
+ ==Homepage
21
+ http://informationcardruby.com
22
+
23
+ See also:
24
+ http://rubyforge.org/projects/informationcard
25
+ http://www.codeplex.com/informationcardruby
26
+
27
+ ==Community
28
+ Discussion regarding the Information Card library takes place on the RubyForge mailing lists
29
+
30
+ http://rubyforge.org/mailman/listinfo/informationcard-users
31
+
32
+ Please join us to discuss, ask questions and report bugs
33
+
34
+ ==Authors
35
+ Joe Poon
36
+ Jason Sallis
37
+
38
+ ==License
39
+ Copyright (c) 2007 ThoughtWorks, released under the BSD license
40
+
41
+
42
+
43
+
44
+
45
+
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ Gem::manage_gems
3
+ require 'rake/gempackagetask'
4
+ require 'rake/testtask'
5
+ require 'rake/clean'
6
+
7
+ CLEAN.include("pkg")
8
+
9
+ # Gemspec reference - http://rubygems.org/read/chapter/20
10
+ spec = Gem::Specification.new do |s|
11
+ s.name = "information_card"
12
+ s.version = "0.1.0"
13
+ s.author = "Joe Poon, Jason Sallis"
14
+ s.email = "informationcard-users@rubyforge.org"
15
+ s.homepage = "http://informationcardruby.com"
16
+ s.platform = Gem::Platform::RUBY
17
+ s.summary = "A library for processing information cards"
18
+ s.files = FileList["lib/**/*", "docs/**/*", "test/**/*", "Rakefile", "LICENSE", "CHANGELOG"].exclude("rdoc").to_a
19
+ s.require_path = "lib"
20
+ s.autorequire = "information_card"
21
+ s.has_rdoc = true
22
+ s.extra_rdoc_files = ["README"]
23
+ end
24
+
25
+ Rake::GemPackageTask.new(spec) do |pkg|
26
+ pkg.need_tar = true
27
+ end
28
+
29
+ Rake::TestTask.new do |t|
30
+ t.libs << "test"
31
+ t.test_files = FileList["test/*test.rb"]
32
+ t.verbose = true
33
+ end
34
+
35
+ task :default => [:test]
@@ -0,0 +1,10 @@
1
+ require 'information_card/claim_types'
2
+ require 'information_card/config'
3
+ require 'information_card/decrypter'
4
+ require 'information_card/identity_token'
5
+ require 'information_card/saml_token'
6
+ require 'information_card/invalid_token'
7
+ require 'information_card/processor'
8
+ require 'information_card/xml_canonicalizer'
9
+ require 'information_card/certificate_util'
10
+ require 'information_card/namespaces'
@@ -0,0 +1,17 @@
1
+ require 'openssl'
2
+
3
+ module InformationCard
4
+ class CertificateUtil
5
+
6
+ def self.lookup_private_key(directory, subject)
7
+ path = File.join(directory, '*.crt')
8
+ Dir[path].each do |cert_file|
9
+ cert = OpenSSL::X509::Certificate.new(File.read(cert_file))
10
+ return OpenSSL::PKey::RSA.new(File.read(cert_file.gsub(/.crt$/, '') + ".key")) if (cert.subject.to_s == subject)
11
+ end
12
+ raise "No private key found in #{path.gsub(/\*.crt/, '')} with subject #{subject}"
13
+ end
14
+ end
15
+ end
16
+
17
+
@@ -0,0 +1,43 @@
1
+ module InformationCard
2
+ class ClaimTypes
3
+
4
+ @@claims = {
5
+ :given_name => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
6
+ :email_address => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
7
+ :surname => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
8
+ :street_address => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress",
9
+ :locality => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality",
10
+ :state_province => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince",
11
+ :postal_code => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode",
12
+ :country => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country",
13
+ :home_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone",
14
+ :other_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone",
15
+ :mobile_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone",
16
+ :date_of_birth => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth",
17
+ :gender => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender",
18
+ :ppid => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier",
19
+ :webpage => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage"
20
+ }
21
+
22
+ def self.map(specified_claims)
23
+ values = []
24
+ specified_claims.each do |claim_key|
25
+ raise "Undefined claim #{claim_key}" if not @@claims.include?(claim_key)
26
+ values << @@claims[claim_key]
27
+ end
28
+ values
29
+ end
30
+
31
+ def self.lookup(namespace, attribute_name)
32
+ # Some identity selector implementations specify the attribute name as part of the namespace.
33
+ # As a result, we need to remove the duplicated attribute name from the namespace.
34
+ # ex. namespace => http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
35
+ # attribute_name => emailaddress
36
+ desired_claim = "#{namespace}/#{attribute_name}".gsub(/#{attribute_name}\/#{attribute_name}/, attribute_name)
37
+ @@claims.each_pair do |key, value|
38
+ return key if value == desired_claim
39
+ end
40
+ raise "Undefined claim #{desired_claim}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,52 @@
1
+ module InformationCard
2
+ class Config
3
+ def self.certificate_location=(certificate_location)
4
+ @certificate_location = certificate_location
5
+ end
6
+
7
+ def self.certificate_location
8
+ @certificate_location
9
+ end
10
+
11
+ def self.certificate_subject=(certificate_subject)
12
+ @certificate_subject = certificate_subject
13
+ end
14
+
15
+ def self.certificate_subject
16
+ @certificate_subject
17
+ end
18
+
19
+ def self.audience_scope=(audience_scope)
20
+ @audience_scope = audience_scope
21
+ end
22
+
23
+ def self.audience_scope
24
+ @audience_scope
25
+ end
26
+
27
+ def self.audiences=(audiences)
28
+ @audiences = audiences
29
+ end
30
+
31
+ def self.audiences
32
+ @audiences
33
+ end
34
+
35
+ def self.required_claims=(required_claims)
36
+ @required_claims = required_claims
37
+ end
38
+
39
+ def self.required_claims
40
+ @required_claims
41
+ end
42
+
43
+ def self.identity_claim=(identity_claim)
44
+ @identity_claim = identity_claim
45
+ end
46
+
47
+ def self.identity_claim
48
+ @identity_claim
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,53 @@
1
+ module InformationCard
2
+ class Decrypter
3
+
4
+ attr_reader :errors
5
+
6
+ def initialize(encrypted_information_card_xml, certificate_location, certificate_subject)
7
+ @xml_document = REXML::Document.new(encrypted_information_card_xml)
8
+ @certificate_location = certificate_location
9
+ @certificate_subject = certificate_subject
10
+ @errors = {}
11
+ end
12
+
13
+ def decrypt
14
+ private_key = CertificateUtil.lookup_private_key(@certificate_location, @certificate_subject)
15
+ encrypted_data = REXML::XPath.first(@xml_document, "enc:EncryptedData", {"enc" => Namespaces::XENC})
16
+ key_info = REXML::XPath.first(encrypted_data, "x:KeyInfo", {"x" => Namespaces::DS})
17
+ encrypted_key = REXML::XPath.first(key_info, "e:EncryptedKey", {"e" => Namespaces::XENC})
18
+ key_cipher = REXML::XPath.first(encrypted_key, "e:CipherData/e:CipherValue", {"e" => Namespaces::XENC})
19
+ key = decrypt_key(key_cipher.text, private_key)
20
+
21
+ cipher_data = REXML::XPath.first(@xml_document, "enc:EncryptedData/enc:CipherData/enc:CipherValue", {"enc" => Namespaces::XENC})
22
+ decrypt_cipher_data(key, cipher_data.text)
23
+ end
24
+
25
+ def valid?
26
+ # TODO: Should perform more validation and handle errors more gracefully.
27
+ # ex. What if algorithm is not supported?
28
+ errors.empty?
29
+ end
30
+
31
+ private
32
+
33
+ def decrypt_key(key_wrap_cipher, private_key, ssl_padding=OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
34
+ # TODO: Encrypted method is assumed t obe rsa-oaep-mgf1p
35
+ from_key = OpenSSL::PKey::RSA.new(private_key)
36
+ key_wrap_str = Base64.decode64(key_wrap_cipher)
37
+ from_key.private_decrypt(key_wrap_str, ssl_padding)
38
+ end
39
+
40
+ def decrypt_cipher_data(key_cipher, cipher_data)
41
+ cipher_data_str = Base64.decode64(cipher_data)
42
+ mcrypt_iv = cipher_data_str[0..15]
43
+ cipher_data_str = cipher_data_str[16..-1]
44
+ # TODO: Encryption method algorithm is assumed to be aes256-cbc.
45
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
46
+ cipher.decrypt
47
+ cipher.key = key_cipher
48
+ cipher.iv = mcrypt_iv
49
+ result = cipher.update(cipher_data_str)
50
+ result << cipher.final
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ module InformationCard
2
+ class IdentityToken
3
+ attr_reader :errors, :claims
4
+
5
+ def initialize
6
+ @errors = {}
7
+ @claims = {}
8
+ end
9
+
10
+ def valid?
11
+ @errors.empty?
12
+ end
13
+
14
+ def unique_id
15
+ nil
16
+ end
17
+
18
+ def ppid
19
+ nil
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,8 @@
1
+ module InformationCard
2
+ class InvalidToken < IdentityToken
3
+ def initialize(errors)
4
+ super()
5
+ @errors = errors
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module InformationCard
2
+ module Namespaces
3
+ XENC = "http://www.w3.org/2001/04/xmlenc#"
4
+ DS = "http://www.w3.org/2000/09/xmldsig#"
5
+ SAML_ASSERTION = "urn:oasis:names:tc:SAML:1.0:assertion"
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module InformationCard
2
+ class Processor
3
+ def self.process(encrypted_information_card_xml)
4
+ begin
5
+ decrypter = Decrypter.new(encrypted_information_card_xml,
6
+ InformationCard::Config.certificate_location,
7
+ InformationCard::Config.certificate_subject)
8
+ decrypted_information_card = decrypter.decrypt
9
+ rescue => e
10
+ return InvalidToken.new({:decryption => e.message})
11
+ end
12
+ SamlToken.create(decrypted_information_card)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,212 @@
1
+ # Portions of this class were inspired by the XmlSecurity::SignedDocument class written by Todd Saxton
2
+ # and the 'Self-Issued InfoCard Tutorial and Demo' written by Kim Cameron
3
+
4
+ require 'rexml/document'
5
+ require 'digest/sha1'
6
+ require 'base64'
7
+
8
+ module InformationCard
9
+ class SamlToken < IdentityToken
10
+ include REXML
11
+
12
+ private :initialize
13
+
14
+ def self.create(saml_input)
15
+ saml_doc = REXML::Document.new(saml_input)
16
+ saml_token = SamlToken.new(saml_doc)
17
+
18
+ saml_token.validate_document_conditions
19
+ saml_token.validate_document_integrity
20
+ return saml_token unless saml_token.valid?
21
+
22
+ saml_token.process_claims
23
+ saml_token.validate_claims
24
+
25
+ saml_token
26
+ end
27
+
28
+ def initialize(saml_doc)
29
+ super()
30
+ @doc = saml_doc
31
+ end
32
+
33
+ def unique_id
34
+ identity_claim_value = @claims[InformationCard::Config.identity_claim]
35
+ return identity_claim_value unless InformationCard::Config.identity_claim == :ppid
36
+
37
+ combined_key = ''
38
+ combined_key << @mod
39
+ combined_key << @exponent
40
+ combined_key << identity_claim_value
41
+ Digest::SHA1.hexdigest(combined_key)
42
+ end
43
+
44
+ def ppid
45
+ claims[:ppid]
46
+ end
47
+
48
+ def process_claims
49
+ attribute_nodes = XPath.match(@doc, "//saml:AttributeStatement/saml:Attribute", {"saml" => Namespaces::SAML_ASSERTION})
50
+ attribute_nodes.each do |node|
51
+ key = ClaimTypes.lookup(node.attributes['AttributeNamespace'], node.attributes['AttributeName'])
52
+ @claims[key] = XPath.first(node, "saml:AttributeValue", "saml" => Namespaces::SAML_ASSERTION).text
53
+ end
54
+ end
55
+
56
+ def validate_claims
57
+ return if Config::required_claims.nil? or Config::required_claims.empty?
58
+
59
+ claims_errors = []
60
+ Config::required_claims.each { |claim| claims_errors << claim if not @claims.key?(claim) }
61
+ @errors[:missing_claims] = claims_errors unless claims_errors.empty?
62
+ end
63
+
64
+ def validate_document_conditions
65
+ validate_audiences
66
+ validate_conditions
67
+ end
68
+
69
+ def validate_document_integrity
70
+ verify_digest
71
+ verify_signature
72
+ end
73
+
74
+ def validate_audiences
75
+ conditions = XPath.first(@doc, "//saml:Conditions", "saml" => Namespaces::SAML_ASSERTION)
76
+ audiences = XPath.match(@doc, "//saml:AudienceRestrictionCondition/saml:Audience", {"saml" => Namespaces::SAML_ASSERTION})
77
+ @errors[:audience] = "AudienceRestriction is not valid" unless valid_audiences?(audiences)
78
+ end
79
+
80
+ def validate_conditions
81
+ conditions = XPath.first(@doc, "//saml:Conditions", "saml" => Namespaces::SAML_ASSERTION)
82
+
83
+ condition_errors = {}
84
+ not_before_time = Time.parse(conditions.attributes['NotBefore'])
85
+ condition_errors[:not_before] = "Time is before #{not_before_time}" if Time.now < not_before_time
86
+
87
+ not_on_or_after_time = Time.parse(conditions.attributes['NotOnOrAfter'])
88
+ condition_errors[:not_on_or_after] = "Time is on or after #{not_on_or_after_time}" if Time.now >= not_on_or_after_time
89
+
90
+ @errors[:conditions] = condition_errors unless condition_errors.empty?
91
+ end
92
+
93
+ def verify_digest
94
+ working_doc = REXML::Document.new(@doc.to_s)
95
+
96
+ assertion_node = XPath.first(working_doc, "saml:Assertion", {"saml" => Namespaces::SAML_ASSERTION})
97
+ signature_node = XPath.first(assertion_node, "ds:Signature", {"ds" => Namespaces::DS})
98
+ signed_info_node = XPath.first(signature_node, "ds:SignedInfo", {"ds" => Namespaces::DS})
99
+ digest_value_node = XPath.first(signed_info_node, "ds:Reference/ds:DigestValue", {"ds" => Namespaces::DS})
100
+
101
+ digest_value = digest_value_node.text
102
+
103
+ signature_node.remove
104
+ digest_errors = []
105
+ canonicalizer = InformationCard::XmlCanonicalizer.new
106
+
107
+ reference_nodes = XPath.match(signed_info_node, "ds:Reference", {"ds" => Namespaces::DS})
108
+ # TODO: Check specification to see if digest is required.
109
+ @errors[:digest] = "No reference nodes to check digest" and return if reference_nodes.nil? or reference_nodes.empty?
110
+
111
+ reference_nodes.each do |node|
112
+ uri = node.attributes['URI']
113
+ nodes_to_verify = XPath.match(working_doc, "saml:Assertion[@AssertionID='#{uri[1..uri.size]}']", {"saml" => Namespaces::SAML_ASSERTION})
114
+
115
+ nodes_to_verify.each do |node|
116
+ canonicalized_signed_info = canonicalizer.canonicalize(node)
117
+ signed_node_hash = Base64.encode64(Digest::SHA1.digest(canonicalized_signed_info)).chomp
118
+ digest_errors << "Invalid Digest for #{uri}. Expected #{signed_node_hash} but was #{digest_value}" unless signed_node_hash == digest_value
119
+ end
120
+
121
+ @errors[:digest] = digest_errors unless digest_errors.empty?
122
+ end
123
+ end
124
+
125
+ def verify_signature
126
+ assertion_node = XPath.first(@doc, "saml:Assertion", {"saml" => Namespaces::SAML_ASSERTION})
127
+ signature_node = XPath.first(assertion_node, "ds:Signature", {"ds" => Namespaces::DS})
128
+ modulus_node = XPath.first(signature_node, "ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Modulus", {"ds" => Namespaces::DS})
129
+ exponent_node = XPath.first(signature_node, "ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Exponent", {"ds" => Namespaces::DS})
130
+
131
+ @mod = modulus_node.text
132
+ @exponent = exponent_node.text
133
+ public_key_string = get_public_key(@mod, @exponent)
134
+
135
+ signed_info_node = XPath.first(signature_node, "ds:SignedInfo", {"ds" => Namespaces::DS})
136
+ signature_value_node = XPath.first(signature_node, "ds:SignatureValue", {"ds" => Namespaces::DS})
137
+
138
+ signature = Base64.decode64(signature_value_node.text)
139
+ canonicalized_signed_info = InformationCard::XmlCanonicalizer.new.canonicalize(signed_info_node)
140
+
141
+ @errors[:signature] = "Invalid Signature" unless public_key_string.verify(OpenSSL::Digest::SHA1.new, signature, canonicalized_signed_info)
142
+ end
143
+
144
+ def valid_audiences?(audiences)
145
+ audience_scope = InformationCard::Config.audience_scope
146
+ registered_audiences = InformationCard::Config.audiences
147
+
148
+ return false if registered_audiences.nil? or registered_audiences.empty?
149
+
150
+ if audience_scope == :page
151
+ audiences.each{|audience| return true if registered_audiences.include?(audience.text)}
152
+ elsif audience_scope == :site
153
+ audiences.each do |audience|
154
+ registered_audiences.each do |registered_audience|
155
+ return true if audience.text.index(registered_audience) == 0
156
+ end
157
+ end
158
+ end
159
+ false
160
+ end
161
+
162
+ def get_public_key(mod, exponent)
163
+ mod_binary = Base64.decode64(mod)
164
+ exponent_binary = Base64.decode64(exponent)
165
+
166
+ exponent_encoding = make_asn_segment(0x02, exponent_binary)
167
+ modulusEncoding = make_asn_segment(0x02, mod_binary)
168
+ sequenceEncoding = make_asn_segment(0x30, modulusEncoding + exponent_encoding)
169
+ bitstringEncoding = make_asn_segment(0x03, sequenceEncoding)
170
+ hex_array = []
171
+ "300D06092A864886F70D0101010500".gsub(/../) { |m| hex_array << m.to_i(16) }
172
+ rsaAlgorithmIdentifier = hex_array.pack('C*')
173
+ combined = rsaAlgorithmIdentifier + bitstringEncoding
174
+ publicKeyInfo = make_asn_segment(0x30, rsaAlgorithmIdentifier + bitstringEncoding)
175
+
176
+ #encode the publicKeyInfo in base64 and add PEM brackets
177
+ public_key_64 = Base64.encode64(publicKeyInfo)
178
+ encoding = "-----BEGIN PUBLIC KEY-----\n"
179
+ offset = 0;
180
+ # strip out the newlines
181
+ public_key_64.delete!("=\n")
182
+ while (segment = public_key_64[offset, 64])
183
+ encoding = encoding + segment + "\n"
184
+ offset += 64
185
+ end
186
+ encoding = encoding + "-----END PUBLIC KEY-----\n"
187
+ @pub_key = OpenSSL::PKey::RSA.new(encoding)
188
+ @pub_key
189
+ end
190
+
191
+ def make_asn_segment(type, string)
192
+ case (type)
193
+ when 0x02
194
+ string = 0.chr + string if string[0] > 0x7f
195
+ when 0x03
196
+ string = 0.chr + string
197
+ end
198
+ length = string.length
199
+
200
+ if (length < 128)
201
+ output = sprintf("%c%c%s", type, length, string)
202
+ elsif (length < 0x0100)
203
+ output = sprintf("%c%c%c%s", type, 0x81, length, string)
204
+ elsif (length < 0x010000)
205
+ output = sprintf("%c%c%c%c%s", type, 0x82, length/0x0100, length%0x0100, string)
206
+ else
207
+ output = nil
208
+ end
209
+ output
210
+ end
211
+ end
212
+ end