add methods for encoding more types

This commit is contained in:
Ben Toews 2019-02-21 15:52:26 -07:00
Родитель a2a17f9337
Коммит 7fdb72a330
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: E9C423BE17EFEE70
2 изменённых файлов: 301 добавлений и 102 удалений

Просмотреть файл

@ -379,12 +379,18 @@ module SSHData
case type
when :string
encode_string(value)
when :list
encode_list(value)
when :mpint
encode_mpint(value)
when :time
encode_time(value)
when :uint64
encode_uint64(value)
when :uint32
encode_uint32(value)
when :options
encode_options(value)
else
raise DecodeError
end
@ -451,6 +457,76 @@ module SSHData
[list, str_read]
end
# Encode a list of strings.
#
# value - The Array of Strings to encode.
#
# Returns an encoded representation of the list.
def encode_list(value)
encode_string(value.map { |s| encode_string(s) }.join)
end
# Read a multi-precision integer from the provided raw data.
#
# raw - A binary String.
# offset - The offset into raw at which to read (default 0).
#
# Returns an Array including the decoded mpint as an OpenSSL::BN and the
# Integer number of bytes read.
def decode_mpint(raw, offset=0)
if raw.bytesize < offset + 4
raise DecodeError, "data too short"
end
str_size_s = raw.byteslice(offset, 4)
str_size = str_size_s.unpack("L>").first
mpi_size = str_size + 4
if raw.bytesize < offset + mpi_size
raise DecodeError, "data too short"
end
mpi_s = raw.slice(offset, mpi_size)
# This calls OpenSSL's BN_mpi2bn() function. As far as I can tell, this
# matches up with with MPI type defined in RFC4251 Section 5 with the
# exception that OpenSSL doesn't enforce minimal length. We could enforce
# this ourselves, but it doesn't seem worth the added complexity.
mpi = OpenSSL::BN.new(mpi_s, 0)
[mpi, mpi_size]
end
# Encode a BN as an mpint.
#
# value - The OpenSSL::BN value to encode.
#
# Returns an encoded representation of the BN.
def encode_mpint(value)
value.to_s(0)
end
# Read a time from the provided raw data.
#
# raw - A binary String.
# offset - The offset into raw at which to read (default 0).
#
# Returns an Array including the decoded Time and the Integer number of
# bytes read.
def decode_time(raw, offset=0)
time_raw, read = decode_uint64(raw, offset)
[Time.at(time_raw), read]
end
# Encode a time.
#
# value - The Time value to encode.
#
# Returns an encoded representation of the Time.
def encode_time(value)
encode_uint64(value.to_i)
end
# Read the specified number of strings out of the provided raw data.
#
# raw - A binary String.
@ -508,56 +584,18 @@ module SSHData
[opts, str_read]
end
# Read a multi-precision integer from the provided raw data.
# Encode series of key/value pairs.
#
# raw - A binary String.
# offset - The offset into raw at which to read (default 0).
# value - The Hash value to encode.
#
# Returns an Array including the decoded mpint as an OpenSSL::BN and the
# Integer number of bytes read.
def decode_mpint(raw, offset=0)
if raw.bytesize < offset + 4
raise DecodeError, "data too short"
# Returns an encoded representation of the Hash.
def encode_options(value)
opts_raw = value.reduce("") do |encoded, (key, value)|
value_str = value == true ? "" : encode_string(value)
encoded + encode_string(key) + encode_string(value_str)
end
str_size_s = raw.byteslice(offset, 4)
str_size = str_size_s.unpack("L>").first
mpi_size = str_size + 4
if raw.bytesize < offset + mpi_size
raise DecodeError, "data too short"
end
mpi_s = raw.slice(offset, mpi_size)
# This calls OpenSSL's BN_mpi2bn() function. As far as I can tell, this
# matches up with with MPI type defined in RFC4251 Section 5 with the
# exception that OpenSSL doesn't enforce minimal length. We could enforce
# this ourselves, but it doesn't seem worth the added complexity.
mpi = OpenSSL::BN.new(mpi_s, 0)
[mpi, mpi_size]
end
# Encoding a BN as an mpint.
#
# value - The OpenSSL::BN value to encode.
#
# Returns an encoded representation of the BN.
def encode_mpint(value)
value.to_s(0)
end
# Read a time from the provided raw data.
#
# raw - A binary String.
# offset - The offset into raw at which to read (default 0).
#
# Returns an Array including the decoded Time and the Integer number of
# bytes read.
def decode_time(raw, offset=0)
time_raw, read = decode_uint64(raw, offset)
[Time.at(time_raw), read]
encode_string(opts_raw)
end
# Read a uint64 from the provided raw data.

Просмотреть файл

@ -2,6 +2,8 @@ require "securerandom"
require_relative "./spec_helper"
describe SSHData::Encoding do
let(:junk) { String.new("\xff\xff", encoding: Encoding::ASCII_8BIT) }
describe "#pem_type" do
let(:type) { "FOO BAR" }
let(:head) { "-----BEGIN #{type}-----" }
@ -566,43 +568,229 @@ describe SSHData::Encoding do
end
end
describe("#decode_options") do
it "can decode options" do
opts = {"k1" => "v1", "k2" => "v2"}
raw_opts = opts.reduce("") do |cum, (k, v)|
cum + [
described_class.encode_string(k),
described_class.encode_string(described_class.encode_string(v))
].join
describe("strings") do
test_cases = []
test_cases << [
:normal, # name
"foobar", # raw
"\x00\x00\x00\x06foobar", # encoded
]
test_cases << [
:empty, # name
"", # raw
"\x00\x00\x00\x00", # encoded
]
test_cases.each do |name, raw, encoded|
describe("#{name} values") do
it "can decode" do
raw2, read = described_class.decode_string(encoded + junk)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can at an offset" do
raw2, read = described_class.decode_string(junk + encoded + junk, junk.bytesize)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can encode" do
encoded2 = described_class.encode_string(raw)
expect(encoded2).to eq(encoded)
end
end
encoded = described_class.encode_string(raw_opts)
decoded, read = described_class.decode_options(encoded)
expect(decoded).to eq(opts)
expect(read).to eq(encoded.bytesize)
encoded = described_class.encode_string("")
decoded, read = described_class.decode_options(encoded)
expect(decoded).to eq({})
expect(read).to eq(encoded.bytesize)
end
end
describe("#decode_list") do
it "can decode a series of strings" do
strs = %w(one two three)
list_raw = strs.map { |s| described_class.encode_string(s) }.join
describe("lists") do
test_cases = []
encoded = described_class.encode_string(list_raw)
decoded, read = described_class.decode_list(encoded)
expect(decoded).to eq(strs)
expect(read).to eq(encoded.bytesize)
test_cases << [
:normal, # name
%w(one two three), # raw
"\x00\x00\x00\x17\x00\x00\x00\x03one\x00\x00\x00\x03two\x00\x00\x00\x05three", # encoded
]
encoded = described_class.encode_string("")
decoded, read = described_class.decode_list(encoded)
expect(decoded).to eq([])
test_cases << [
:empty, # name
%w(), # raw
"\x00\x00\x00\x00", # encoded
]
test_cases.each do |name, raw, encoded|
describe("#{name} values") do
it "can decode" do
raw2, read = described_class.decode_list(encoded + junk)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can decode at an offset" do
raw2, read = described_class.decode_list(junk + encoded + junk, junk.bytesize)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can encode" do
encoded2 = described_class.encode_list(raw)
expect(encoded2).to eq(encoded)
end
end
end
end
describe("mpint") do
test_cases = []
test_cases << [
:positive, # name
OpenSSL::BN.new(0x01020304), # raw
String.new("\x00\x00\x00\x04\x01\x02\x03\x04", encoding: Encoding::ASCII_8BIT) # encoded
]
test_cases << [
:zero, # name
OpenSSL::BN.new(0x00), # raw
String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT) # encoded
]
test_cases.each do |name, raw, encoded|
describe("#{name} values") do
it "can decode" do
raw2, read = described_class.decode_mpint(encoded + junk)
expect(raw2.to_i).to eq(raw.to_i)
expect(read).to eq(encoded.bytesize)
end
it "can decode at an offset" do
raw2, read = described_class.decode_mpint(junk + encoded + junk, junk.bytesize)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can encode" do
encoded2 = described_class.encode_mpint(raw)
expect(encoded2).to eq(encoded)
end
end
end
end
describe("time") do
let(:raw) { Time.at((rand * 1000000000).to_i) }
let(:encoded) { [raw.to_i].pack("Q>") }
let(:junk) { String.new("\xff\xff", encoding: Encoding::ASCII_8BIT) }
it "can decode" do
raw2, read = described_class.decode_time(encoded + junk)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can decode at an offset" do
raw2, read = described_class.decode_time(junk + encoded + junk, junk.bytesize)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can encode" do
encoded2 = described_class.encode_time(raw)
expect(encoded2).to eq(encoded)
end
end
describe("uint64") do
let(:raw) { 0x1234567890abcdef }
let(:encoded) { String.new("\x12\x34\x56\x78\x90\xab\xcd\xef", encoding: Encoding::ASCII_8BIT) }
let(:junk) { String.new("\xff\xff", encoding: Encoding::ASCII_8BIT) }
it "can decode" do
raw2, read = described_class.decode_uint64(encoded + junk)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can decode at an offset" do
raw2, read = described_class.decode_uint64(junk + encoded + junk, junk.bytesize)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can encode" do
encoded2 = described_class.encode_uint64(raw)
expect(encoded2).to eq(encoded)
end
end
describe("uint32") do
let(:raw) { 0x12345678 }
let(:encoded) { String.new("\x12\x34\x56\x78", encoding: Encoding::ASCII_8BIT) }
let(:junk) { String.new("\xff\xff", encoding: Encoding::ASCII_8BIT) }
it "can decode" do
raw2, read = described_class.decode_uint32(encoded + junk)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can decode at an offset" do
raw2, read = described_class.decode_uint32(junk + encoded + junk, junk.bytesize)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can encode" do
encoded2 = described_class.encode_uint32(raw)
expect(encoded2).to eq(encoded)
end
end
describe("options") do
test_cases = []
test_cases << [
:normal, # name
{"k1" => "v1", "k2" => true, "k3" => "v3"}, # raw
[ # encoded
"\x00\x00\x00\x2a",
"\x00\x00\x00\x02", "k1",
"\x00\x00\x00\x06", "\x00\x00\x00\x02", "v1",
"\x00\x00\x00\x02", "k2",
"\x00\x00\x00\x00",
"\x00\x00\x00\x02", "k3",
"\x00\x00\x00\x06", "\x00\x00\x00\x02", "v3",
].join
]
test_cases << [
:empty, # name
{}, # raw
"\x00\x00\x00\x00" # encoded
]
test_cases.each do |name, raw, encoded|
describe("#{name} values") do
it "can decode" do
raw2, read = described_class.decode_options(encoded + junk)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can at an offset" do
raw2, read = described_class.decode_options(junk + encoded + junk, junk.bytesize)
expect(raw2).to eq(raw)
expect(read).to eq(encoded.bytesize)
end
it "can encode" do
encoded2 = described_class.encode_options(raw)
expect(encoded2).to eq(encoded)
end
end
end
end
describe("#decode_n_strings") do
@ -618,31 +806,4 @@ describe SSHData::Encoding do
expect(read).to eq(0)
end
end
describe("#decode_string") do
it "can round trip" do
s1 = "foobar"
s2, read = described_class.decode_string(described_class.encode_string(s1))
expect(s2).to eq(s1)
expect(read).to eq(s1.length + 4)
end
end
describe("#decode_mpint") do
it "can round trip" do
i1 = OpenSSL::BN.new(SecureRandom.bytes((rand * 100).to_i), 2)
i2, read = described_class.decode_mpint(described_class.encode_mpint(i1))
expect(i2).to eq(i1)
end
end
describe("#decode_time") do
it "can round trip" do
t1 = Time.at((rand * 1000000000).to_i)
t2, read = described_class.decode_time([t1.to_i].pack("Q>"))
expect(t2).to eq(t1)
expect(read).to eq(8)
end
end
end