The outer layer of a PPK file is text-based. The PuTTY tools will always use LF line termination when writing PPK files, but will tolerate CR+LF and CR-only on input.
The first few lines identify it as a PPK, and give some initial data about what's stored in it and how. They look like this:
PuTTY-User-Key-File-version: algorithm-name
Encryption: encryption-type
Comment: key-comment-string
version is a decimal number giving the version number of the file format itself. The current file format version is 3.
algorithm-name is the SSH protocol identifier for the public key algorithm that this key is used for (such as ‘ssh-dss
’ or ‘ecdsa-sha2-nistp384
’).
encryption-type indicates whether this key is stored encrypted, and if so, by what method. Currently the only supported encryption types are ‘aes256-cbc
’ and ‘none
’.
key-comment-string is a free text field giving the comment. This can contain any byte values other than 13 and 10 (CR and LF).
The next part of the file gives the public key. This is stored unencrypted but base64-encoded (RFC 4648), and is preceded by a header line saying how many lines of base64 data are shown, looking like this:
Public-Lines: number-of-lines
that many lines of base64 data
The base64-encoded data in this blob is formatted in exactly the same way as an SSH public key sent over the wire in the SSH protocol itself. That is also the same format as the base64 data stored in OpenSSH's authorized_keys
file, except that in a PPK file the base64 data is split across multiple lines. But if you remove the newlines from the middle of this section, the resulting base64 blob is in the right format to go in an authorized_keys
line.
If the key is encrypted (i.e. encryption-type is not ‘none
’), then the next thing that appears is a sequence of lines specifying how the keys for encrypting the file are to be derived from the passphrase:
Key-Derivation: argon2-flavour
Argon2-Memory: decimal-integer
Argon2-Passes: decimal-integer
Argon2-Parallelism: decimal-integer
Argon2-Salt: hex-string
argon2-flavour is one of the identifiers ‘Argon2d
’, ‘Argon2i
’ or ‘Argon2id
’, all describing variants of the Argon2 password-hashing function.
The three integer values are used as parameters for Argon2, which allows you to configure the amount of memory used (in Kbyte), the number of passes of the algorithm to run (to tune its running time), and the degree of parallelism required by the hash function. The salt is decoded into a sequence of binary bytes and used as an additional input to Argon2. (It is chosen randomly when the key file is written, so that a guessing attack can't be mounted in parallel against multiple key files.)
The next part of the file gives the private key. This is base64-encoded in the same way:
Private-Lines: number-of-lines
that many lines of base64 data
The binary data represented in this base64 blob may be encrypted, depending on the encryption-type field in the key file header shown above:
none
’, then this data is stored in plain text.
aes256-cbc
’, then this data is encrypted using AES, with a 256-bit key length, in the CBC cipher mode. The key and initialisation vector are derived from the passphrase: see section C.4.
In order to encrypt the private key data with AES, it must be a multiple of 16 bytes (the AES cipher block length). This is achieved by appending random padding to the data before encrypting it. When decoding it after decryption, the random data can be ignored: the internal structure of the data is enough to tell you when you've reached the end of the meaningful part.
Unlike public keys, the binary encoding of private keys is not specified at all in the SSH standards. See section C.3 for details of the private key format for each key type supported by PuTTY.
The final thing in the key file is the MAC:
Private-MAC: hex-mac-data
hex-mac-data is a hexadecimal-encoded value, 64 digits long (i.e. 32 bytes), generated using the HMAC-SHA-256 algorithm with the following binary data as input:
string
: the algorithm-name header field.
string
: the encryption-type header field.
string
: the key-comment-string header field.
string
: the binary public key data, as decoded from the base64 lines after the ‘Public-Lines
’ header.
string
: the plaintext of the binary private key data, as decoded from the base64 lines after the ‘Private-Lines
’ header. If that data was stored encrypted, then the decrypted version of it is used in this MAC preimage, including the random padding mentioned above.
The MAC key is derived from the passphrase: see section C.4.