diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9c1df4d..57dfa78 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1 +1,2 @@ * Kévin Lacointe (Some GnuPG patches) +* Stephen Paul Weber diff --git a/README.md b/README.md index f6dd6b6..b757b36 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ Contributors ------------ * [Kévin Lacointe](mailto:kevinlacointe@gmail.com) - +* [Stephen Paul Weber](mailto:singpolyma@singpolyma.net) - Contributing ------------ diff --git a/bin/keygen b/bin/keygen new file mode 100755 index 0000000..54f9aac --- /dev/null +++ b/bin/keygen @@ -0,0 +1,17 @@ +#!/usr/bin/ruby +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))) +require 'openpgp' + +nkey = OpenSSL::PKey::RSA.new(1024) + +nkey = OpenPGP::Packet::SecretKey.new(:key => nkey.params.merge({ + :u => OpenPGP.egcd(nkey.params['p'].to_i, nkey.params['q'].to_i) +}).inject({}) {|c, (k, v)| + c.merge!({k.intern => OpenPGP.bn2bin(v.to_i)}) +}, :algorithm => OpenPGP::Algorithm::Asymmetric::RSA, :version => 4, :timestamp => Time.now.to_i) + +uid = OpenPGP::Packet::UserID.new(:name => 'Test', :email => 'test@example.com') + +m = OpenPGP::Engine::OpenSSL::RSA.new(nkey).sign_key_userid([nkey, uid]) + +print m.to_s diff --git a/lib/openpgp.rb b/lib/openpgp.rb index e259f25..a9e8c25 100644 --- a/lib/openpgp.rb +++ b/lib/openpgp.rb @@ -12,6 +12,14 @@ end end +unless 1.respond_to?(:ord) + class Numeric + def ord + to_i + end + end +end + module OpenPGP require 'openpgp/util' diff --git a/lib/openpgp/engine/openssl.rb b/lib/openpgp/engine/openssl.rb index 35f817b..3f29368 100644 --- a/lib/openpgp/engine/openssl.rb +++ b/lib/openpgp/engine/openssl.rb @@ -17,6 +17,172 @@ def self.install! [Random, Digest].each { |mod| install_extensions! mod } end + ## + # Wrap OpenSSL RSA methods for use with OpenPGP + class RSA + def initialize(packet) + packet = OpenPGP::Message::parse(packet) if packet.is_a?(String) + @key = @message = nil + if packet.is_a?(OpenPGP::Packet::PublicKey) || \ + (packet.respond_to?(:first) && packet.first.is_a?(OpenPGP::Packet::PublicKey)) + @key = packet + else + @message = packet + end + end + + ## + # @param [String] keyid (Optional) + # @return OpenPGP::Packet::PublicKey + def key(keyid=nil) + return nil unless @key + keyid.upcase! if keyid + if @key.is_a?(Enumerable) # Like an OpenPGP::Message + @key.select {|p| p.is_a?(OpenPGP::Packet::PublicKey) && (!keyid || \ + p.fingerprint[keyid.length*-1,keyid.length].upcase == keyid) + }.first + end || @key + end + + ## + # @param [String] keyid (Optional) + # @return OpenSSL::PKey::RSA + def rsa_key(keyid=nil) + self.class.convert_key(key(keyid)) + end + + ## + # @param packet message to verify with @key, or key (OpenPGP or RSA) to check @message with + # @param [Integer] index specify which signature to verify (if there is more than one) + # @return Boolean + def verify(packet, index=0) + packet = OpenPGP::Message::parse(packet) if packet.is_a?(String) + if packet.is_a?(OpenPGP::Message) && !packet.first.is_a?(OpenPGP::Packet::PublicKey) + m = packet + k = self + else + m = @message + k = self.class.new(packet) + end + + return nil unless m + signature_packet, data_packet = m.signature_and_data(index) + k = k.rsa_key(signature_packet.issuer) + return nil unless k && signature_packet.key_algorithm_name == 'RSA' + + return m.verify({'RSA' => {signature_packet.hash_algorithm_name => lambda {|m,s| + k.verify(signature_packet.hash_algorithm_name, s.first, m) + }}}) + end + + ## + # @param packet message to sign with @key or key (OpenPGP or RSA) to sign @message with + # @param [String] hash name of hash function to use (default SHA256) + # @param [String] keyid id of key to use (if there is more than one) + # @return OpenPGP::Message + def sign(packet, hash='SHA256', keyid=nil) + packet = unless packet.is_a?(OpenPGP::Packet) || packet.is_a?(OpenPGP::Message) + if @key + OpenPGP::Packet::LiteralData.new(:data => packet, :timestamp => Time.now.to_i) + else + OpenPGP::Message::parse(packet) + end + else + packet + end + + if packet.is_a?(OpenPGP::Packet::SecretKey) || packet.is_a?(::OpenSSL::PKey::RSA) \ + || (packet.is_a?(Enumerable) && packet.first.is_a?(OpenPGP::Packet::SecretKey)) + m = @message + k = packet + else + m = packet + k = @key + end + + return nil unless k && m # Missing some data + + m = m.signature_and_data.first if m.is_a?(OpenPGP::Message) + + unless k.is_a?(::OpenSSL::PKey::RSA) + k = self.class.new(k) + keyid = k.key.fingerprint[-16,16] unless keyid + k = k.rsa_key(keyid) + end + + sig = OpenPGP::Packet::Signature.new(:version => 4, + :key_algorithm => OpenPGP::Algorithm::Asymmetric::RSA, + :hash_algorithm => OpenPGP::Digest::for(hash).to_i) + sig.hashed_subpackets << OpenPGP::Packet::Signature::Issuer.new(keyid) + sig.hashed_subpackets << OpenPGP::Packet::Signature::SignatureCreationTime.new(Time.now.to_i) + sig.sign_data(m, {'RSA' => {hash => lambda {|m| k.sign(hash, m)}}}) + OpenPGP::Message.new([sig, m]) + end + + ## + # @param packet message with key/userid packets to sign (@key must be set) + # @param [String] hash name of hash function to use (default SHA256) + # @param [String] keyid id of key to use (if there is more than one) + # @return OpenPGP::Message + def sign_key_userid(packet, hash='SHA256', keyid=nil) + if packet.is_a?(String) + packet = OpenPGP::Message.parse(packet) + elsif !packet.is_a?(OpenPGP::Message) + packet = OpenPGP::Message.new(packet) + end + + return nil unless @key && packet # Missing some data + + keyid = key.fingerprint[-16,16] unless keyid + + sig = packet.signature_and_data[1] + unless sig + sig = OpenPGP::Packet::Signature.new(:version => 4, + :key_algorithm => OpenPGP::Algorithm::Asymmetric::RSA, + :hash_algorithm => OpenPGP::Digest::for(hash).to_i, + :type => 0x13) + sig.hashed_subpackets << OpenPGP::Packet::Signature::SignatureCreationTime.new(Time.now.to_i) + sig.hashed_subpackets << OpenPGP::Packet::Signature::KeyFlags.new(0x01 | 0x02) + sig.unhashed_subpackets << OpenPGP::Packet::Signature::Issuer.new(keyid) + packet << sig + end + sig.sign_data(packet, {'RSA' => {hash => lambda {|m| rsa_key.sign(hash, m)}}}) + + packet + end + + ## + # @param packet + # @return [OpenSSL::PKey::RSA] + def self.convert_key(packet) + # packet is already an key + return packet if packet.is_a?(::OpenSSL::PKey::RSA) + unless packet.is_a?(Hash) + # Get the first item in a message + packet = packet.first if packet.is_a?(Enumerable) + # TODO: Error if packet.algorithm not RSA + packet = packet.key # Get key material + end + + # Create blank key and fill the fields + key = ::OpenSSL::PKey::RSA.new + packet.each {|k,v| + next if k == :u # OpenSSL doesn't call it that + if v.is_a?(Numeric) + v = ::OpenSSL::BN.new(v.to_s) + elsif !(v.is_a?(::OpenSSL::BN)) + # Convert the byte string to an OpenSSL::BN + v = v.reverse.enum_for(:each_char).enum_for(:each_with_index) \ + .inject(::OpenSSL::BN.new('0')) {|c, (b,i)| + c + (b.force_encoding('binary').ord << i*8) + } + end + key.send("#{k}=".intern, v) + } + key + end + end + ## # @private module Random #:nodoc: diff --git a/lib/openpgp/message.rb b/lib/openpgp/message.rb index 4eadd6c..7cf7e24 100644 --- a/lib/openpgp/message.rb +++ b/lib/openpgp/message.rb @@ -86,6 +86,35 @@ def initialize(*packets, &block) block.call(self) if block_given? end + def signature_and_data(index=0) + msg = self + msg = msg.first while msg.first.is_a?(OpenPGP::Packet::CompressedData) + signature_packet = data_packet = nil + i = 0 + msg.each { |packet| + if packet.is_a?(OpenPGP::Packet::Signature) + signature_packet = packet if i == index + i += 1 + elsif packet.is_a?(OpenPGP::Packet::LiteralData) + data_packet = packet + end + break if signature_packet && data_packet + } + [signature_packet, data_packet] + end + + ## + # @param verifiers a Hash of callables formatted like {'RSA' => {'SHA256' => callable}} that take two parameters: message and signature + # @param index signature number to verify (if more than one) + def verify(verifiers, index=0) + signature_packet, data_packet = signature_and_data(index) + return nil unless signature_packet && data_packet # No signature or no data + verifier = verifiers[signature_packet.key_algorithm_name][signature_packet.hash_algorithm_name] + return nil unless verifier # No verifier + data_packet.normalize + verifier.call(data_packet.data + signature_packet.trailer, signature_packet.fields) + end + ## # @yield [packet] # @yieldparam [Packet] packet @@ -124,11 +153,7 @@ def size def to_s Buffer.write do |buffer| packets.each do |packet| - if body = packet.body - buffer.write_byte(packet.class.tag | 0xC0) - buffer.write_byte(body.size) - buffer.write_bytes(body) - end + buffer.write_bytes(packet.to_s) end end end diff --git a/lib/openpgp/packet.rb b/lib/openpgp/packet.rb index c4f8df9..2cdab9a 100644 --- a/lib/openpgp/packet.rb +++ b/lib/openpgp/packet.rb @@ -1,3 +1,5 @@ +require 'zlib' + module OpenPGP ## # OpenPGP packet. @@ -105,12 +107,24 @@ def initialize(options = {}, &block) block.call(self) if block_given? end - #def to_s() body end + def to_s + data = header_and_body + data[:header] + (data[:body] || '') + end ## # @return [Integer] def size() body.size end + ## + # @return [Hash] + def header_and_body + body = self.body # Get body first, we will need it's length + tag = (self.class.tag | 0xC0).chr # First two bits are 1 for new packet format + size = 255.chr + [body ? body.length : 0].pack('N') # Use 5-octet lengths + {:header => tag + size, :body => body} + end + ## # @return [String] def body @@ -145,8 +159,17 @@ class Signature < Packet attr_accessor :key_algorithm, :hash_algorithm attr_accessor :key_id attr_accessor :fields + attr_accessor :hashed_subpackets, :unhashed_subpackets + attr_accessor :hash_head, :trailer + + def initialize(options={}, &blk) + @hashed_subpackets = [] + @unhashed_subpackets = [] + super(options, &blk) + end def self.parse_body(body, options = {}) + @hashed_subpackets = @unhashed_subpackets = [] case version = body.read_byte when 3 then self.new(:version => 3).send(:read_v3_signature, body) when 4 then self.new(:version => 4).send(:read_v4_signature, body) @@ -154,6 +177,76 @@ def self.parse_body(body, options = {}) end end + ## + # @params [OpenPGP::Packet::LiteralData | OpenPGP::Message] m + # @params [Hash] signers in the same format as verifiers for Message + def sign_data(m, signers) + data = if m.is_a?(LiteralData) + self.type = m.format == :b ? 0x00 : 0x01 + m.normalize # Line endings + m.data + else + # m must be message where PublicKey is first, UserID is second + m = m.to_a # Se we can index into it + key = m[0].fingerprint_material.join + user_id = m[1].body + key + 0xB4.chr + [user_id.length].pack('N') + user_id + end + update_trailer + signer = signers[key_algorithm_name][hash_algorithm_name] + self.fields = signer.call(data + trailer) + self.fields = [fields] unless fields.is_a?(Enumerable) + self.hash_head = fields.first[0,2].unpack('n').first + end + + def key_algorithm_name + name = OpenPGP::Algorithm::Asymmetric::constants.select do |const| + OpenPGP::Algorithm::Asymmetric::const_get(const) == key_algorithm + end.first + name = :RSA if name == :RSA_S || name == :RSA_E + name.to_s + end + + def hash_algorithm_name + OpenPGP::Digest::for(hash_algorithm).algorithm.to_s + end + + def issuer + packet = (hashed_subpackets + unhashed_subpackets).select {|packet| + packet.is_a?(OpenPGP::Packet::Signature::Issuer) + }.first + if packet + packet.data + else + key_id + end + end + + def update_trailer + @trailer = body(true) + end + + def body(trailer=false) + body = 4.chr + type.chr + key_algorithm.chr + hash_algorithm.chr + + sub = hashed_subpackets.inject('') {|c,p| c + p.to_s} + body << [sub.length].pack('n') + sub + + # The trailer is just the top of the body plus some crap + return body + 4.chr + 0xff.chr + [body.length].pack('N') if trailer + + sub = unhashed_subpackets.inject('') {|c,p| c + p.to_s} + body << [sub.length].pack('n') + sub + + body << [hash_head].pack('n') + + fields.each {|data| + body << [OpenPGP.bitlength(data)].pack('n') + body << data + } + body + end + protected ## @@ -162,7 +255,7 @@ def read_v3_signature(body) raise "Invalid OpenPGP signature packet V3 header" if body.read_byte != 5 @type, @timestamp, @key_id = body.read_byte, body.read_number(4), body.read_number(8, 16) @key_algorithm, @hash_algorithm = body.read_byte, body.read_byte - body.read_bytes(2) + @hash_head = body.read_bytes(2) read_signature(body) self end @@ -172,13 +265,58 @@ def read_v3_signature(body) def read_v4_signature(body) @type = body.read_byte @key_algorithm, @hash_algorithm = body.read_byte, body.read_byte - body.read_bytes(hashed_count = body.read_number(2)) - body.read_bytes(unhashed_count = body.read_number(2)) - body.read_bytes(2) + # We store exactly the original trailer for doing verifications + @trailer = 4.chr + type.chr + key_algorithm.chr + hash_algorithm.chr + hashed_count = body.read_number(2) + hashed_data = body.read_bytes(hashed_count) + @trailer << [hashed_count].pack('n') + hashed_data + 4.chr + 0xff.chr + [6 + hashed_count].pack('N') + @hashed_subpackets = read_subpackets(Buffer.new(hashed_data)) + unhashed_count = body.read_number(2) + unhashed_data = body.read_bytes(unhashed_count) + @unhashed_subpackets = read_subpackets(Buffer.new(unhashed_data)) + @hash_head = body.read_bytes(2).unpack('n').first read_signature(body) self end + ## + # @see http://tools.ietf.org/html/rfc4880#section-5.2.3.1 + def read_subpackets(buf) + packets = [] + until buf.eof? + if packet = read_subpacket(buf) + packets << packet + else + raise "Invalid OpenPGP message data at position #{buf.pos} in signature subpackets" + end + end + packets + end + + def read_subpacket(buf) + length = buf.read_byte.ord + length_of_length = 1 + # if len < 192 One octet length, no furthur processing + if length > 190 && length < 255 # Two octet length + length_of_length = 2 + length = ((length - 192) << 8) + buf.read_byte.ord + 192 + end + if length == 255 # Five octet length + length_of_length = 5 + length = buf.read_unpacked(4, 'N') + end + + tag = buf.read_byte.ord + critical = (tag & 0x80) != 0 + tag &= 0x7F + self.class.const_get(self.class.constants.select {|t| + self.class.const_get(t).const_defined?(:TAG) && \ + self.class.const_get(t)::TAG == tag + }.first).parse_body(Buffer.new(buf.read(length-1)), :tag => tag) + rescue Exception + nil # Parse error, return no subpacket + end + ## # @see http://tools.ietf.org/html/rfc4880#section-5.2.2 def read_signature(body) @@ -191,6 +329,181 @@ def read_signature(body) raise "Unknown OpenPGP signature packet public-key algorithm: #{key_algorithm}" end end + + class Subpacket < Packet + attr_reader :data + def header_and_body + b = body + # Use 5-octet lengths + size = 255.chr + [body.length+1].pack('N') + tag = self.class.const_get(:TAG).chr + {:header => size + tag, :body => body} + end + end + + ## + # @see http://tools.ietf.org/html/rfc4880#section-5.2.3.4 + class SignatureCreationTime < Subpacket + TAG = 2 + def initialize(time=nil) + super() + @data = time || Time.now.to_i + end + + def self.parse_body(body, options={}) + self.new(body.read_timestamp) + end + + def body + [@data].pack('N') + end + end + class SignatureExpirationTime < Subpacket + TAG = 3 + def initialize(time=nil) + super() + @data = time || 0 + end + + def self.parse_body(body, options={}) + self.new(body.read_timestamp) + end + + def body + [@data].pack('N') + end + end + class ExportableCertification < Subpacket + TAG = 4 + end + class TrustSignature < Subpacket + TAG = 5 + end + class RegularExpression < Subpacket + TAG = 6 + end + class Revocable < Subpacket + TAG = 7 + end + class KeyExpirationTime < Subpacket + TAG = 9 + def initialize(time=nil) + super() + @data = time || Time.now.to_i + end + + def self.parse_body(body, options={}) + self.new(body.read_timestamp) + end + + def body + [@data].pack('N') + end + end + class PreferredSymmetricAlgorithms < Subpacket + TAG = 11 + end + class RevocationKey < Subpacket + TAG = 12 + end + + ## + # @see http://tools.ietf.org/html/rfc4880#section-5.2.3.5 + class Issuer < Subpacket + TAG = 16 + def initialize(keyid=nil) + super() + @data = keyid + end + + def self.parse_body(body, options={}) + data = '' + 8.times do # Store KeyID in Hex + data << '%02X' % body.read_byte.ord + end + self.new(data) + end + + def body + b = '' + @data.enum_for(:each_char).each_slice(2) do |i| + b << i.join.to_i(16).chr + end + b + end + end + class NotationData < Subpacket + TAG = 20 + end + class PreferredHashAlgorithms < Subpacket + TAG = 21 + end + class PreferredCompressionAlgorithms < Subpacket + TAG = 22 + end + + ## + # @see http://tools.ietf.org/html/rfc4880#section-5.2.3.18 + class KeyServerPreferences < Subpacket + TAG = 23 + end + class PreferredKeyServer < Subpacket + TAG = 24 + def initialize(uri=nil) + super() + @data = uri + end + + def self.parse_body(body, options={}) + self.new(body.read) + end + + def body + @data + end + end + class PrimaryUserID < Subpacket + TAG = 25 + end + class PolicyURI < Subpacket + TAG = 26 + end + class KeyFlags < Subpacket + TAG = 27 + attr_accessor :flags + + def initialize(*flags) + super() + @flags = flags.flatten + end + + def self.parse_body(body, options={}) + flags = [] + until body.eof? + flags << body.read_byte.ord + end + self.new(flags) + end + + def body + flags.map {|f| f.chr}.join + end + end + class SignersUserID < Subpacket + TAG = 28 + end + class ReasonforRevocation < Subpacket + TAG = 29 + end + class Features < KeyFlags + TAG = 30 + end + class SignatureTarget < Subpacket + TAG = 31 + end + class EmbeddedSignature < Subpacket + TAG = 32 + end end ## @@ -243,7 +556,7 @@ class OnePassSignature < Packet class PublicKey < Packet attr_accessor :size attr_accessor :version, :timestamp, :algorithm - attr_accessor :key, :key_fields, :key_id, :fingerprint + attr_accessor :key, :key_id, :fingerprint #def parse(data) # FIXME def self.parse_body(body, options = {}) @@ -262,14 +575,29 @@ def self.parse_body(body, options = {}) ## # @see http://tools.ietf.org/html/rfc4880#section-5.5.2 def read_key_material(body) - @key_fields = case algorithm + key_fields.each { |field| key[field] = body.read_mpi } + @key_id = fingerprint[-8..-1] + end + + def key_fields + case algorithm when Algorithm::Asymmetric::RSA then [:n, :e] when Algorithm::Asymmetric::ELG_E then [:p, :g, :y] when Algorithm::Asymmetric::DSA then [:p, :q, :g, :y] else raise "Unknown OpenPGP key algorithm: #{algorithm}" end - @key_fields.each { |field| key[field] = body.read_mpi } - @key_id = fingerprint[-8..-1] + end + + def fingerprint_material + case version + when 2, 3 + [key[:n], key[:e]].join + when 4 + material = key_fields.map do |key_field| + [[OpenPGP.bitlength(key[key_field])].pack('n'), key[key_field]] + end.flatten.join + [0x99.chr, [material.length + 6].pack('n'), version.chr, [timestamp].pack('N'), algorithm.chr, material] + end end ## @@ -278,14 +606,18 @@ def read_key_material(body) def fingerprint @fingerprint ||= case version when 2, 3 - Digest::MD5.hexdigest([key[:n], key[:e]].join).upcase + Digest::MD5.hexdigest(fingerprint_material.join).upcase when 4 - material = [0x99.chr, [size].pack('n'), version.chr, [timestamp].pack('N'), algorithm.chr] - key_fields.each do |key_field| - material << [OpenPGP.bitlength(key[key_field])].pack('n') - material << key[key_field] - end - Digest::SHA1.hexdigest(material.join).upcase + Digest::SHA1.hexdigest(fingerprint_material.join).upcase + end + end + + def body + case version + when 2, 3 + # TODO + when 4 + fingerprint_material[2..-1].join end end end @@ -309,7 +641,83 @@ class PublicSubkey < PublicKey # @see http://tools.ietf.org/html/rfc4880#section-11.2 # @see http://tools.ietf.org/html/rfc4880#section-12 class SecretKey < PublicKey - # TODO + attr_accessor :s2k_useage, :symmetric_type, :s2k_type, :s2k_hash_algorithm + attr_accessor :s2k_salt, :s2k_count, :encrypted_data, :data + + def self.parse_body(body, options={}) + key = super # All the fields from PublicKey + data = {:s2k_useage => body.read_byte.ord} + if data[:s2k_useage] == 255 || data[:s2k_useage] == 254 + data[:symmetric_type] = body.read_byte.ord + data[:s2k_type] = body.read_byte.ord + data[:s2k_hash_algorithm] = self.read_byte.ord + if data[:s2k_type] == 1 || data[:s2k_type] == 3 + data[:s2k_salt] = body.read_bytes(8) + end + if data[:s2k_type] == 3 + c = self.read_byte.ord + data[:s2k_count] = (16 + (c & 15)).floor << ((c >> 4) + 6) + end + elsif data[:s2k_useage] > 0 + data[:symmetric_type] = data[:s2k_useage] + end + if data[:s2k_useage] > 0 + # TODO: IV of the same length as cipher's block size + data[:encrypted_data] = body.read # Rest of input is MPIs and checksum (encrypted) + else + data[:data] = body.read # Rest of input is MPIs and checksum + end + data.each {|k,v| key.send("#{k}=", v) } + key.key_from_data + key + end + + def key_from_data + return nil unless data # Not decrypted yet + body = Buffer.new(data) + secret_key_fields.each {|mpi| + self.key[mpi] = body.read_mpi + } + # TODO: Validate checksum? + if s2k_useage == 254 # 20 octet sha1 hash + @private_hash = body.read_bytes(20) + else # 2 octet checksum + @private_hash = body.read_bytes(2) + end + end + + def secret_key_fields + case algorithm + when Algorithm::Asymmetric::RSA, + Algorithm::Asymmetric::RSA_E, + Algorithm::Asymmetric::RSA_S then [:d, :p, :q, :u] + when Algorithm::Asymmetric::ELG_E then [:x] + when Algorithm::Asymmetric::DSA then [:x] + else raise "Unknown OpenPGP key algorithm: #{algorithm}" + end + end + + def body + super + s2k_useage.to_i.chr + \ + if s2k_useage == 255 || s2k_useage == 254 + symmetric_type.chr + s2k_type.chr + s2k_hash_algorithm.chr + \ + (s2k_type == 1 || s2k_type == 3 ? s2k_salt : '') + # (s2k_type == 3 ? reverse ugly bit manipulation + end.to_s + if s2k_useage.to_i > 0 + encrypted_data + else + secret_material = secret_key_fields.map {|f| [OpenPGP.bitlength(key[f].to_s)].pack('n') + key[f].to_s}.join + end + \ + if s2k_useage == 254 # SHA1 checksum + # TODO + "\0"*20 + else # 2-octet checksum + # TODO, this design will not work for encrypted keys + [secret_material.split(//).inject(0) {|chk, c| + chk = (chk + c.ord) % 65536 + }].pack('n') + end + end end ## @@ -328,7 +736,48 @@ class SecretSubkey < SecretKey # # @see http://tools.ietf.org/html/rfc4880#section-5.6 class CompressedData < Packet - # TODO + include Enumerable + attr_accessor :algorithm, :data + + def initialize(algorithm=nil, data=nil) + @algorithm = algorithm + @data = data + end + + def self.parse_body(body, options={}) + algorithm = body.read_byte.ord + data = body.read + data = Message::parse(case algorithm + when 0 # Uncompressed + data + when 1 # ZIP + Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(data) + when 2 # ZLIB + Zlib::Inflate.inflate(data) + when 3 # BZIP2 + # TODO + end) + self.new(algorithm, data) + end + + def body + body = algorithm.chr + body << case algorithm + when 0 # Uncompressed + data.to_s + when 1 # ZIP + Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(data.to_s, Zlib::FINISH) + when 2 # ZLIB + Zlib::Deflate.deflate(data.to_s) + when 3 # BZIP2 + # TODO + end + end + + # Proxy onto embedded OpenPGP message + def each(&cb) + @data.each &cb + end end ## @@ -386,6 +835,15 @@ def initialize(options = {}, &block) super(defaults.merge(options), &block) end + def normalize + # Normalize line endings + if format == :u || format == :t + @data.gsub!(/\r\n/, "\n") + @data.gsub!(/\r/, "\n") + @data.gsub!(/\n/, "\r\n") + end + end + def write_body(buffer) buffer.write_byte(format) buffer.write_string(filename) @@ -446,7 +904,7 @@ def write_body(buffer) buffer.write(to_s) end - def to_s + def body text = [] text << name if name text << "(#{comment})" if comment diff --git a/lib/openpgp/util.rb b/lib/openpgp/util.rb index 67164d7..96392cf 100644 --- a/lib/openpgp/util.rb +++ b/lib/openpgp/util.rb @@ -64,6 +64,53 @@ def self.crc24(data) # @return [Integer] # @see http://tools.ietf.org/html/rfc4880#section-3.2 def self.bitlength(data) - data.empty? ? 0 : (data.size - 1) * 8 + (Math.log(data[0].ord) / Math.log(2)).floor + 1 + data = data.split(//) + while (f = data.shift) == '\0'; end + return 0 unless f + Math.log(f.ord, 2).floor + 1 + (data.length*8) end + + ## + # Returns the network-byte-order representation of n + # @param [Numeric] n + # @return [String] + def self.bn2bin(n) + raise RangeError.new('Cannot convert negative number') if n < 0 + bytes = n.size + + # Mask off any leading 0 bytes + mask = 0xFF << (8 * bytes - 1) + while (mask & n) == 0 + mask >>= 8 + bytes -= 1 + end + + result = [] + bits_left = n + until bits_left == 0 + result << (bits_left & 0xFF).chr + bits_left >>= 8 + end + result.reverse.join + end + + ## + # Returns the multiplicative inverse of b, mod m + # @param [Numeric] b + # @param [Numeric] m + # @return [Numeric] + def self.egcd(b,m,recLevel=0) + if b % m == 0 + [0,1] + else + tmpVal = egcd(m, b % m, recLevel+1) + tmpVal2 = [tmpVal[1], tmpVal[0]-tmpVal[1] * ((b/m).to_i)] + if recLevel == 0 + tmpVal2[0] % m + else + tmpVal2 + end + end + end + end