Class: PuTTY::Key::PPK
- Inherits:
-
Object
- Object
- PuTTY::Key::PPK
- Defined in:
- lib/putty/key/ppk.rb
Overview
Represents a PuTTY private key (.ppk) file.
The PPK constructor can be used to either create an
uninitialized key or to read a .ppk file (from file or an IO
-like
instance).
The #save method can be used to write a PPK instance to a .ppk file or
IO
-like instance.
The #algorithm, #comment, #public_blob and #private_blob
attributes provide access to the high level fields of the PuTTY private
key as binary String
instances. The structure of the two blobs will vary
based on the algorithm.
Encrypted .ppk files can be read and written by specifying a passphrase when loading or saving. Files are encrypted using AES in CBC mode with a 256-bit key derived from the passphrase.
The PPK class supports files corresponding to PuTTY's formats 2 and 3.
Format 1 (not supported) was only used briefly early on in the development
of the .ppk format and was never released. Format 2 is supported by PuTTY
version 0.52 onwards. Format 3 is supported by PuTTY version 0.75 onwards.
#save defaults to format 2. Use the format
parameter to select
format 3.
libargon2 (https://github.com/P-H-C/phc-winner-argon2) is required to load and save encrypted format 3 files. Binaries are typically available with your OS distribution. For Windows, binaries are available at https://github.com/philr/argon2-windows/releases - use either Argon2OptDll.dll for CPUs supporting AVX or Argon2RefDll.dll otherwise.
Constant Summary collapse
- DEFAULT_ENCRYPTION_TYPE =
The default (and only supported) encryption algorithm.
'aes256-cbc'.freeze
- DEFAULT_FORMAT =
The default PuTTY private key file format.
2
- MINIMUM_FORMAT =
The mimimum supported PuTTY private key file format.
2
- MAXIMUM_FORMAT =
The maximum supported PuTTY private key file format.
3
- DEFAULT_ARGON2_PARAMS =
Default Argon2 key derivation parameters for use with format 3.
Argon2Params.new.freeze
Instance Attribute Summary collapse
-
#algorithm ⇒ String
The key's algorithm, for example, 'ssh-rsa' or 'ssh-dss'.
-
#comment ⇒ String
A comment to describe the PuTTY private key.
-
#private_blob ⇒ String
The private component of the key (after decryption when loading and before encryption when saving).
-
#public_blob ⇒ String
Get or set the public component of the key.
Instance Method Summary collapse
-
#initialize(path_or_io = nil, passphrase = nil) ⇒ PPK
constructor
Constructs a new PPK instance either uninitialized, or initialized by reading from a .ppk file or an
IO
-like instance. -
#save(path_or_io, passphrase = nil, encryption_type: DEFAULT_ENCRYPTION_TYPE, format: DEFAULT_FORMAT, argon2_params: DEFAULT_ARGON2_PARAMS) ⇒ Integer
Writes this PuTTY private key instance to a .ppk file or
IO
-like instance.
Constructor Details
#initialize(path_or_io = nil, passphrase = nil) ⇒ PPK
Constructs a new PuTTY::Key::PPK instance either uninitialized, or initialized
by reading from a .ppk file or an IO
-like instance.
To read from a file set path_or_io
to the file path, either as a
String
or a Pathname
. To read from an IO
-like instance set
path_or_io
to the instance. The instance must respond to #read
.
#binmode
will be called before reading if supported by the instance.
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 |
# File 'lib/putty/key/ppk.rb', line 109 def initialize(path_or_io = nil, passphrase = nil) passphrase = nil if passphrase && passphrase.to_s.empty? if path_or_io Reader.open(path_or_io) do |reader| format, @algorithm = reader.field_matching(/PuTTY-User-Key-File-(\d+)/) format = format.to_i raise FormatError, "The ppk file is using a format that is too new (#{format})" if format > MAXIMUM_FORMAT raise FormatError, "The ppk file is using an old unsupported format (#{format})" if format < MINIMUM_FORMAT encryption_type = reader.field('Encryption') @comment = reader.field('Comment') @public_blob = reader.blob('Public') if encryption_type == 'none' passphrase = nil mac_key = derive_keys(format).first @private_blob = reader.blob('Private') else raise FormatError, "The ppk file is encrypted with #{encryption_type}, which is not supported" unless encryption_type == DEFAULT_ENCRYPTION_TYPE raise ArgumentError, 'The ppk file is encrypted, a passphrase must be supplied' unless passphrase argon2_params = if format >= 3 type = get_argon2_type(reader.field('Key-Derivation')) memory = reader.unsigned_integer('Argon2-Memory', maximum: 2**32) passes = reader.unsigned_integer('Argon2-Passes', maximum: 2**32) parallelism = reader.unsigned_integer('Argon2-Parallelism', maximum: 2**32) salt = reader.field('Argon2-Salt') unless salt =~ /\A(?:[0-9a-fA-F]{2})+\z/ raise FormatError, "Expected the Argon2-Salt field to be a hex string, but found #{salt}" end Argon2Params.new(type: type, memory: memory, passes: passes, parallelism: parallelism, salt: [salt].pack('H*')) end cipher = ::OpenSSL::Cipher::AES.new(256, :CBC) cipher.decrypt mac_key, cipher.key, cipher.iv = derive_keys(format, cipher, passphrase, argon2_params) cipher.padding = 0 encrypted_private_blob = reader.blob('Private') @private_blob = if encrypted_private_blob.bytesize > 0 partial = cipher.update(encrypted_private_blob) final = cipher.final partial + final else encrypted_private_blob end end private_mac = reader.field('Private-MAC') expected_private_mac = compute_private_mac(format, mac_key, encryption_type, @private_blob) unless private_mac == expected_private_mac raise ArgumentError, 'Incorrect passphrase supplied' if passphrase raise FormatError, "Invalid Private MAC (expected #{expected_private_mac}, but found #{private_mac})" end end end end |
Instance Attribute Details
#algorithm ⇒ String
The key's algorithm, for example, 'ssh-rsa' or 'ssh-dss'.
65 66 67 |
# File 'lib/putty/key/ppk.rb', line 65 def algorithm @algorithm end |
#comment ⇒ String
A comment to describe the PuTTY private key.
70 71 72 |
# File 'lib/putty/key/ppk.rb', line 70 def comment @comment end |
#private_blob ⇒ String
The private component of the key (after decryption when loading and before encryption when saving).
Note that when loading an encrypted .ppk file, this may include additional 'random' suffix used as padding.
84 85 86 |
# File 'lib/putty/key/ppk.rb', line 84 def private_blob @private_blob end |
#public_blob ⇒ String
Get or set the public component of the key.
75 76 77 |
# File 'lib/putty/key/ppk.rb', line 75 def public_blob @public_blob end |
Instance Method Details
#save(path_or_io, passphrase = nil, encryption_type: DEFAULT_ENCRYPTION_TYPE, format: DEFAULT_FORMAT, argon2_params: DEFAULT_ARGON2_PARAMS) ⇒ Integer
Writes this PuTTY private key instance to a .ppk file or IO
-like
instance.
To write to a file, set path_or_io
to the file path, either as a
String
or a Pathname
. To write to an IO
-like instance set
path_or_io
to the instance. The instance must respond to #write
.
#binmode
will be called before writing if supported by the instance.
If a file with the given path already exists, it will be overwritten.
The #algorithm, #private_blob and #public_blob attributes must have been set before calling #save.
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/putty/key/ppk.rb', line 213 def save(path_or_io, passphrase = nil, encryption_type: DEFAULT_ENCRYPTION_TYPE, format: DEFAULT_FORMAT, argon2_params: DEFAULT_ARGON2_PARAMS) raise InvalidStateError, 'algorithm must be set before calling save' unless @algorithm raise InvalidStateError, 'public_blob must be set before calling save' unless @public_blob raise InvalidStateError, 'private_blob must be set before calling save' unless @private_blob raise ArgumentError, 'An output path or io instance must be specified' unless path_or_io passphrase = nil if passphrase && passphrase.to_s.empty? raise ArgumentError, 'A format must be specified' unless format raise ArgumentError, "Unsupported format: #{format}" unless format >= MINIMUM_FORMAT && format <= MAXIMUM_FORMAT if passphrase raise ArgumentError, 'An encryption_type must be specified if a passphrase is specified' unless encryption_type raise ArgumentError, "Unsupported encryption_type: #{encryption_type}" unless encryption_type == DEFAULT_ENCRYPTION_TYPE raise ArgumentError, 'argon2_params must be specified if a passphrase is specified with format 3' unless format < 3 || argon2_params cipher = ::OpenSSL::Cipher::AES.new(256, :CBC) cipher.encrypt mac_key, cipher.key, cipher.iv, kdf_params = derive_keys(format, cipher, passphrase, argon2_params) cipher.padding = 0 # Pad using an SHA-1 hash of the unpadded private blob in order to # prevent an easily known plaintext attack on the last block. padding_length = cipher.block_size - ((@private_blob.bytesize - 1) % cipher.block_size) - 1 padded_private_blob = @private_blob padded_private_blob += ::OpenSSL::Digest::SHA1.new(@private_blob).digest.byteslice(0, padding_length) if padding_length > 0 encrypted_private_blob = if padded_private_blob.bytesize > 0 partial = cipher.update(padded_private_blob) final = cipher.final partial + final else padded_private_blob end else encryption_type = 'none' mac_key = derive_keys(format).first kdf_params = nil padded_private_blob = @private_blob encrypted_private_blob = padded_private_blob end private_mac = compute_private_mac(format, mac_key, encryption_type, padded_private_blob) Writer.open(path_or_io) do |writer| writer.field("PuTTY-User-Key-File-#{format}", @algorithm) writer.field('Encryption', encryption_type) writer.field('Comment', @comment) writer.blob('Public', @public_blob) if kdf_params # Only Argon2 is currently supported. writer.field('Key-Derivation', "Argon2#{kdf_params.type}") writer.field('Argon2-Memory', kdf_params.memory) writer.field('Argon2-Passes', kdf_params.passes) writer.field('Argon2-Parallelism', kdf_params.parallelism) writer.field('Argon2-Salt', kdf_params.salt.unpack('H*').first) end writer.blob('Private', encrypted_private_blob) writer.field('Private-MAC', private_mac) end end |