information_card 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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