summaryrefslogtreecommitdiff
path: root/lib/net/ssh/authentication/ed25519.rb
blob: c2a117a31dbc6b6f10235a104402f406377ada7f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
gem 'ed25519', '~> 1.2'
gem 'bcrypt_pbkdf', '~> 1.0' unless RUBY_PLATFORM == "java"

require 'ed25519'

require 'base64'

require 'net/ssh/transport/cipher_factory'
require 'net/ssh/authentication/pub_key_fingerprint'
require 'bcrypt_pbkdf' unless RUBY_PLATFORM == "java"

module Net
  module SSH
    module Authentication
      module ED25519
        class SigningKeyFromFile < SimpleDelegator
          def initialize(pk, sk)
            key = ::Ed25519::SigningKey.from_keypair(sk)
            raise ArgumentError, "pk does not match sk" unless pk == key.verify_key.to_bytes

            super(key)
          end
        end

        class OpenSSHPrivateKeyLoader
          CipherFactory = Net::SSH::Transport::CipherFactory

          MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
          MEND = "-----END OPENSSH PRIVATE KEY-----"
          MAGIC = "openssh-key-v1"

          class DecryptError < ArgumentError
            def initialize(message, encrypted_key: false)
              super(message)
              @encrypted_key = encrypted_key
            end

            def encrypted_key?
              return @encrypted_key
            end
          end

          def self.read(datafull, password)
            datafull = datafull.strip
            raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
            raise ArgumentError.new("Expected #{MEND} at end of private key") unless datafull.end_with?(MEND)

            datab64 = datafull[MBEGIN.size...-MEND.size]
            data = Base64.decode64(datab64)
            raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC)

            buffer = Net::SSH::Buffer.new(data[MAGIC.size + 1..-1])

            ciphername = buffer.read_string
            raise ArgumentError.new("#{ciphername} in private key is not supported") unless
              CipherFactory.supported?(ciphername)

            kdfname = buffer.read_string
            raise ArgumentError.new("Expected #{kdfname} to be or none or bcrypt") unless %w[none bcrypt].include?(kdfname)

            kdfopts = Net::SSH::Buffer.new(buffer.read_string)
            num_keys = buffer.read_long
            raise ArgumentError.new("Only 1 key is supported in ssh keys #{num_keys} was in private key") unless num_keys == 1

            _pubkey = buffer.read_string

            len = buffer.read_long

            keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true)
            raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if
              ((len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0))

            if kdfname == 'bcrypt'
              salt = kdfopts.read_string
              rounds = kdfopts.read_long

              raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java"

              key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds)
            else
              key = '\x00' * (keylen + ivlen)
            end

            cipher = CipherFactory.get(ciphername, key: key[0...keylen], iv: key[keylen...keylen + ivlen], decrypt: true)

            decoded = cipher.update(buffer.remainder_as_buffer.to_s)
            decoded << cipher.final

            decoded = Net::SSH::Buffer.new(decoded)
            check1 = decoded.read_long
            check2 = decoded.read_long

            raise DecryptError.new("Decrypt failed on private key", encrypted_key: kdfname == 'bcrypt') if (check1 != check2)

            type_name = decoded.read_string
            case type_name
            when "ssh-ed25519"
              PrivKey.new(decoded)
            else
              decoded.read_private_keyblob(type_name)
            end
          end
        end

        class PubKey
          include Net::SSH::Authentication::PubKeyFingerprint

          attr_reader :verify_key

          def initialize(data)
            @verify_key = ::Ed25519::VerifyKey.new(data)
          end

          def self.read_keyblob(buffer)
            PubKey.new(buffer.read_string)
          end

          def to_blob
            Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string, @verify_key.to_bytes).to_s
          end

          def ssh_type
            "ssh-ed25519"
          end

          def ssh_signature_type
            ssh_type
          end

          def ssh_do_verify(sig, data, options = {})
            @verify_key.verify(sig, data)
          end

          def to_pem
            # TODO this is not pem
            ssh_type + Base64.encode64(@verify_key.to_bytes)
          end
        end

        class PrivKey
          CipherFactory = Net::SSH::Transport::CipherFactory

          MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
          MEND = "-----END OPENSSH PRIVATE KEY-----\n"
          MAGIC = "openssh-key-v1"

          attr_reader :sign_key

          def initialize(buffer)
            pk = buffer.read_string
            sk = buffer.read_string
            _comment = buffer.read_string

            @pk = pk
            @sign_key = SigningKeyFromFile.new(pk, sk)
          end

          def to_blob
            public_key.to_blob
          end

          def ssh_type
            "ssh-ed25519"
          end

          def ssh_signature_type
            ssh_type
          end

          def public_key
            PubKey.new(@pk)
          end

          def ssh_do_sign(data)
            @sign_key.sign(data)
          end

          def self.read(data, password)
            OpenSSHPrivateKeyLoader.read(data, password)
          end
        end
      end
    end
  end
end