2

Here https://security.stackexchange.com/a/52564 you can read that newer OpenSSH versions use bcrypt for protecting the keyfile. Security of bcrypt depends on the costfactor see https://security.stackexchange.com/questions/139721/estimate-the-time-to-crack-passwords-using-bcrypt/201965#201965

According to https://crypto.stackexchange.com/questions/58536/how-does-openssh-use-bcrypt-to-set-ivs/58543#58543 the default bcrypt round number would be 16. This would be a good security. But how to get the round count / cost factor?

What I've done so far: Key looks like (to make it shorter here only a weak 1024 bit key)

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBLF8sO2Q
hcLXI43z96e1hiAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQC0gBWeZpej
9ILT/59bEb0/lSvXx0WfZqP2lXRDbuY+gluuWyT+REQcVTR2BxSx9F/P20mLTnupzY+XE3
xEu+SIJlwKIAH3fed62+QBzDrPsl9kyfoIGIvi/28ZftqVN/kg0GSOaAqu4Px+vNVX1VKn
PNV5VVCZWL4ZPlGQZ48UJwAAAhCwDkueKT9oq8E0qtD92/4DSAD2eTI7bd6jBGUxugEw85
6xWbRYnFQZdwO2ZCNV0aTHViD1FRKlC9cBHDoSORKcM/9dY9Msy6lZj7Tp5s8r7x2pOrJi
TVRbv5/cI732I+l/vYvssJEhZpeSw4JKh9tyPpifVmzBxqtqwkBrTuLCMqkwLmrcxReFUq
aA/RIZy3L616CJsAvx2ezEc49D6SbJ9i9OlKuv73a1baS4RpMvFzWGLE2NBvvtQpEnJFoL
Kyjz+two4doT6SZ7UtiVGyCtO5WQEoeAgjhkbZzOPtM2AvoV+hNLRIX2/52jOB5A1bNQ0v
qW64aj2YNe8vWfj5xtA/8BlyEG7gwhu+0HgbgMDxw7o/0qVkHM/Hv3YgBTRygsH+8h4wsR
kxA292NOKKaD18tv1j3atR80q0XQVcQH20uX8tSqXtKfDtkUc/EPbFCNp3xQJG/F81USKh
YAmjxEeDkZZ/LkEOEJKvFRCL3gFlH4rqF5/pRk6HmB99xceD4irbazm+BWfPAf5Q0zdB5L
/yei3sqA4G48yRXIkaELtYNEeTYHMp3PGz1b3CP3l+ZGZp6XNaM+sMfdICbI3Zae5bnxKg
VXEE2UMdi7DEXbqEzSlfcIf5QzXHMQJm0ZL+iLoaEmakamAxCKk6jJ+QzHzGADZEIRXrj3
5Nhhd0jsToEMsXmmawt2qxy0cIHET1M=
-----END OPENSSH PRIVATE KEY-----

PW is test

Then lets decode the base64. Therefore first and last line beginning with '-----' have to be removed

cat key | tail -n +2 | head -n -1 | base64 -d > text.txt

Now open text.txt e.g. in Notepad++ This shows enter image description here

but now I have no idea how to read the roundcount from there. Can you assist?

Hannes
  • 307
  • 2
  • 12

2 Answers2

2

Take your base64, decode it into hex:

6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31 00 00 00 00 0a 61 65 73 32 35 36 2d 63 74 72 00 00 00 06 
62 63 72 79 70 74 00 00 00 18 00 00 00 10 4b 17 cb 0e d9 08 5c 2d 72 38 df 3f 7a 7b 58 62 00 00 00 
10 00 00 00 01 00 00 00 97 00 00 00 07 73 73 68 2d 72 73 61 00 00 00 03 01 00 01 00 00 00 81 00 b4 
80 15 9e 66 97 a3 f4 82 d3 ff 9f 5b 11 bd 3f 95 2b d7 c7 45 9f 66 a3 f6 95 74 43 6e e6 3e 82 5b ae 
5b 24 fe 44 44 1c 55 34 76 07 14 b1 f4 5f cf db 49 8b 4e 7b a9 cd 8f 97 13 7c 44 bb e4 88 26 5c 0a 
20 01 f7 7d e7 7a db e4 01 cc 3a cf b2 5f 64 c9 fa 08 18 8b e2 ff 6f 19 7e da 95 37 f9 20 d0 64 8e 
68 0a ae e0 fc 7e bc d5 57 d5 52 a7 3c d5 79 55 50 99 58 be 19 3e 51 90 67 8f 14 27 00 00 02 10 b0 
0e 4b 9e 29 3f 68 ab c1 34 aa d0 fd db fe 03 48 00 f6 79 32 3b 6d de a3 04 65 31 ba 01 30 f3 9e b1 
59 b4 58 9c 54 19 77 03 b6 64 23 55 d1 a4 c7 56 20 f5 15 12 a5 0b d7 01 1c 3a 12 39 12 9c 33 ff 5d 
63 d3 2c cb a9 59 8f b4 e9 e6 cf 2b ef 1d a9 3a b2 62 4d 54 5b bf 9f dc 23 bd f6 23 e9 7f bd 8b ec 
b0 91 21 66 97 92 c3 82 4a 87 db 72 3e 98 9f 56 6c c1 c6 ab 6a c2 40 6b 4e e2 c2 32 a9 30 2e 6a dc 
c5 17 85 52 a6 80 fd 12 19 cb 72 fa d7 a0 89 b0 0b f1 d9 ec c4 73 8f 43 e9 26 c9 f6 2f 4e 94 ab af 
ef 76 b5 6d a4 b8 46 93 2f 17 35 86 2c 4d 8d 06 fb ed 42 91 27 24 5a 0b 2b 28 f3 fa dc 28 e1 da 13 
e9 26 7b 52 d8 95 1b 20 ad 3b 95 90 12 87 80 82 38 64 6d 9c ce 3e d3 36 02 fa 15 fa 13 4b 44 85 f6 
ff 9d a3 38 1e 40 d5 b3 50 d2 fa 96 eb 86 a3 d9 83 5e f2 f5 9f 8f 9c 6d 03 ff 01 97 21 06 ee 0c 21 
bb ed 07 81 b8 0c 0f 1c 3b a3 fd 2a 56 41 cc fc 7b f7 62 00 53 47 28 2c 1f ef 21 e3 0b 11 93 10 36 
f7 63 4e 28 a6 83 d7 cb 6f d6 3d da b5 1f 34 ab 45 d0 55 c4 07 db 4b 97 f2 d4 aa 5e d2 9f 0e d9 14 
73 f1 0f 6c 50 8d a7 7c 50 24 6f c5 f3 55 12 2a 16 00 9a 3c 44 78 39 19 67 f2 e4 10 e1 09 2a f1 51 
08 bd e0 16 51 f8 ae a1 79 fe 94 64 e8 79 81 f7 dc 5c 78 3e 22 ad b6 b3 9b e0 56 7c f0 1f e5 0d 33 
74 1e 4b ff 27 a2 de ca 80 e0 6e 3c c9 15 c8 91 a1 0b b5 83 44 79 36 07 32 9d cf 1b 3d 5b dc 23 f7 
97 e6 46 66 9e 97 35 a3 3e b0 c7 dd 20 26 c8 dd 96 9e e5 b9 f1 2a 05 57 10 4d 94 31 d8 bb 0c 45 db 
a8 4c d2 95 f7 08 7f 94 33 5c 73 10 26 6d 19 2f e8 8b a1 a1 26 6a 46 a6 03 10 8a 93 a8 c9 f9 0c c7 
cc 60 03 64 42 11 5e b8 f7 e4 d8 61 77 48 ec 4e 81 0c b1 79 a6 6b 0b 76 ab 1c b4 70 81 c4 4f 53 

The spec defines the format of this data. We can then pick data apart:

    byte[]  AUTH_MAGIC = "openssh-key-v1\n"
    string  ciphername
    string  kdfname
    string  kdfoptions
    int     number of keys N
    string  publickey1
    string  publickey2
    ...
    string  publickeyN
    string  encrypted, padded list of private keys
6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31 00        AUTH_MAGIC "openssh-key-v1\n"
00 00 00 0a                     CipherName string length. 0x0000000a = 10 characters
61 65 73 32 35 36 2d 63 74 72               CipherName "aes256-ctr"
00 00 00 06                     KdfName length prefix  0x00000006 = 6 characters
62 63 72 79 70 74                   KdfName "bcrypt"
00 00 00 18                     KdfOptions string length prefix. 0x00000018 = 24 characters 
00 00 00 10 4b 17 cb 0e d9 08 5c 2d 
72 38 df 3f 7a 7b 58 62 00 00 00 10 

00 00 00 01                     Number of keys. 0x00000001 = 1;

00 00 00 97                     publicKey1 string length  0x00000097
00 00 00 07 73 73 68 2d 72 73 61 00 00 00 03 01     publicKey1
00 01 00 00 00 81 00 b4 80 15 9e 66 97 a3 f4 82 
d3 ff 9f 5b 11 bd 3f 95 2b d7 c7 45 9f 66 a3 f6 
95 74 43 6e e6 3e 82 5b ae 5b 24 fe 44 44 1c 55 
34 76 07 14 b1 f4 5f cf db 49 8b 4e 7b a9 cd 8f 
97 13 7c 44 bb e4 88 26 5c 0a 20 01 f7 7d e7 7a 
db e4 01 cc 3a cf b2 5f 64 c9 fa 08 18 8b e2 ff 
6f 19 7e da 95 37 f9 20 d0 64 8e 68 0a ae e0 fc 
7e bc d5 57 d5 52 a7 3c d5 79 55 50 99 58 be 19 
3e 51 90 67 8f 14 27 
00 00 02 10                     publicKey2 string length 0x00000210 = frazillion characters
b0 0e 4b 9e 29 3f 68 ab c1 34 aa d0 fd db fe 03 
48 00 f6 79 32 3b 6d de a3 04 65 31 ba 01 30 f3 
9e b1 59 b4 58 9c 54 19 77 03 b6 64 23 55 d1 a4 
c7 56 20 f5 15 12 a5 0b d7 01 1c 3a 12 39 12 9c 
33 ff 5d 63 d3 2c cb a9 59 8f b4 e9 e6 cf 2b ef 
1d a9 3a b2 62 4d 54 5b bf 9f dc 23 bd f6 23 e9 
7f bd 8b ec b0 91 21 66 97 92 c3 82 4a 87 db 72 
3e 98 9f 56 6c c1 c6 ab 6a c2 40 6b 4e e2 c2 32 
a9 30 2e 6a dc c5 17 85 52 a6 80 fd 12 19 cb 72 
fa d7 a0 89 b0 0b f1 d9 ec c4 73 8f 43 e9 26 c9 
f6 2f 4e 94 ab af ef 76 b5 6d a4 b8 46 93 2f 17 
35 86 2c 4d 8d 06 fb ed 42 91 27 24 5a 0b 2b 28 
f3 fa dc 28 e1 da 13 e9 26 7b 52 d8 95 1b 20 ad 
3b 95 90 12 87 80 82 38 64 6d 9c ce 3e d3 36 02 
fa 15 fa 13 4b 44 85 f6 ff 9d a3 38 1e 40 d5 b3 
50 d2 fa 96 eb 86 a3 d9 83 5e f2 f5 9f 8f 9c 6d 
03 ff 01 97 21 06 ee 0c 21 bb ed 07 81 b8 0c 0f 
1c 3b a3 fd 2a 56 41 cc fc 7b f7 62 00 53 47 28 
2c 1f ef 21 e3 0b 11 93 10 36 f7 63 4e 28 a6 83 
d7 cb 6f d6 3d da b5 1f 34 ab 45 d0 55 c4 07 db 
4b 97 f2 d4 aa 5e d2 9f 0e d9 14 73 f1 0f 6c 50 
8d a7 7c 50 24 6f c5 f3 55 12 2a 16 00 9a 3c 44 
78 39 19 67 f2 e4 10 e1 09 2a f1 51 08 bd e0 16 
51 f8 ae a1 79 fe 94 64 e8 79 81 f7 dc 5c 78 3e 
22 ad b6 b3 9b e0 56 7c f0 1f e5 0d 33 74 1e 4b 
ff 27 a2 de ca 80 e0 6e 3c c9 15 c8 91 a1 0b b5 
83 44 79 36 07 32 9d cf 1b 3d 5b dc 23 f7 97 e6 
46 66 9e 97 35 a3 3e b0 c7 dd 20 26 c8 dd 96 9e 
e5 b9 f1 2a 05 57 10 4d 94 31 d8 bb 0c 45 db a8 
4c d2 95 f7 08 7f 94 33 5c 73 10 26 6d 19 2f e8 
8b a1 a1 26 6a 46 a6 03 10 8a 93 a8 c9 f9 0c c7 
cc 60 03 64 42 11 5e b8 f7 e4 d8 61 77 48 ec 4e 
81 0c b1 79 a6 6b 0b 76 ab 1c b4 70 81 c4 4f 53 

KDF Options string

The part you want is the string kdfOptions:

00 00 00 10 4b 17 cb 0e d9 08 5c 2d 72 38 df 3f 7a 7b 58 62 00 00 00 10

Which the spec explains:

    string salt
    uint32 rounds

In other words:

00 00 00 10                  Salt string length 0x00000010 = 16 characters
4b 17 cb 0e d9 08 5c 2d 72 38 df 3f 7a 7b 58 62   Salt
00 00 00 10                  Number of rounds. 0x00000010 = 16 

I doubt they mean "rounds". I assume they meant CostFactor.

  • cost factor 16 ⇒ rounds = 216 = 65,535 rounds
  • cost factor 4 ⇒ rounds = 24 = 16 rounds

But there it is.

Bcrypt isn't a KDF

BCrypt is not a key-derivation function; it is a password storage function. You cannot use bcrypt to generate a "key". For example if you wanted "derive" an AES-256 bit key: bcrypt cannot do it.

That's because bcrypt is not a key derivation function.
BCrypt is a password hashing function.

Mis-using bcrypt in this way is an abomination - and a crime against humanity.

More about bcrypt being a password hashing function and not a key derivation function can be found here https://crypto.stackexchange.com/a/70783

Ian Boyd
  • 5,293
  • 14
  • 60
  • 82
  • Actually, OpenSSH [uses](https://github.com/openssh/openssh-portable/blob/V_9_1_P1/sshkey.c#L3974-L3977) a KDF called [`bcrypt_pbkdf(3)`](https://man.openbsd.org/bcrypt_pbkdf.3), which is a slightly modified implementation of PBKDF2. – uasi Oct 14 '22 at 19:08
  • @uasi Yes, that is similar to `scrypt`. Scrypt **is** `PBKDF2(password, ExpensiveFunction(salt), 1)`. And OpenSSH is doing something similar: using `PBKDF2(password, bcrypt(salt), 1)` as the password-based key derivation function - because bcrypt is not a key-derivation function; it is a **password hashing** algorithm. – Ian Boyd Oct 14 '22 at 19:30
  • From your answer, I got the impression that OpenSSH incorrectly uses bcrypt as a KDF. Meybe the Bcrypt isn't a KDF section could be improved. – uasi Oct 14 '22 at 20:04
  • The *'bcrypt isn't a KDF"* section already is expanded. **"More about bcrypt being a password hashing function and not a key derivation function can be found [here](https://crypto.stackexchange.com/a/70783/2126)"** – Ian Boyd Oct 16 '22 at 01:43
  • I think it's irrelevant? Because bcrypt_pbkdf doesn't just do`PBKDF2(password, bcrypt(salt), 1)`, but does `PBKDF2>(password, salt, kdfOptions.rounds)` (where bcrypt_hash is [slightly modified](https://github.com/openssh/openssh-portable/blob/V_9_1_P1/openbsd-compat/bcrypt_pbkdf.c#L48-L55) from the bcrypt password hashing function, and PBKDF2 itself is also modified). bcrypt_hash is used as a replacement to HMAC-SHA1 here, so pointing out it is a password hashing function is not much useful. – uasi Oct 16 '22 at 05:28
  • A little more clarification on `kdfOptions.rounds`: it _is_ PBKDF2 iterations. For each iteration, PDKDF2 hashes input using bcrypt_hash with fixed cost factor of 6. So key deviation rounds is `kdfOptions.rounds * 2^6` in total. – uasi Oct 16 '22 at 05:43
0

Some additional information. To recap the password protection of new ssh-keys is quite secure.

When creating the key or changing the password you can use -a <number of rounds>.

time ssh-keygen -f 2.key -p -a 500 -P b -N b #needs 10 seconds
time ssh-keygen -f 2.key -p -a 1000 -P b -N b #needs 15 seconds
time ssh-keygen -f 2.key -p -a 1000 -P b -N b #needs 20 seconds
time ssh-keygen -f 2.key -p -a 1000 -P b -N b #needs 20 seconds, 10 to encrypt, 10 to decrypt

This was done on an old i3-3220 and needs about half the time on Ryzen 7 5700U. So rounds is indeed rounds and not a cost factor.

To find out if your key is new format use cat key | tail -n +2 | head -n -1 | base64 -d | head -n 2 If you see aes256-ctrbcrypt then your key is in the new format.

To get an estimate of how secure it is against brute force password guessing I used John the ripper. Hashcat has currently (March 2022) no support for this format, see https://hashcat.net/forum/thread-10662.html Prebuilt binaries for John the ripper also might not include the neccessary module for cracking this new kind of ssh key format.

After you have compiled John the ripper go to run directory and create hash from your SSH keyfile via python3 ssh2john.py <keyfile> > hash.txt

Now start John the ripper via ./john hash.txt. Program will use a shipped password list. On AMD Ryzen 7 5700U it can try about 132 PW/s (c/s) which is a really low number. This is CPU only. When changing rounds to 32 via -a 32 number is halfed (65 PW/s) as expected.

To get an estimate what would be possible with GPU I also used a MD5 crypt hash and with this John the Ripper was able to try about 712,000 passwords per second, so about 5400 times faster.

When you compare this to GTX1080 which is capable of about 10 million passwords per second with MD5Crypt https://gist.github.com/epixoip/a83d38f412b4737e99bbef804a270c40 So if this would be linear, GTX 1080 is about 14 times faster than Ryzen 7 5700U, so only about 1850 PW/s should be possible.

So current sshkey encryption is quite safe against offline attacks.

To the end a little python script that takes in filename="test.key" your ssh key and if it is bcrypt format it shows the salt and cost factor.

#!/usr/bin/python3

import binascii
import base64
import re
filename="2.key"
with open(filename, 'r') as f:
    content = f.read()
content = content.split("\n")
content=content[1:-1]
content="".join(content)
#print(content)
decoded=base64.b64decode(content)
#print(decoded)
hex=binascii.hexlify(decoded)
#print(type(hex))
#print(hex)
bycryptPattern="626372797074"
begin=str(hex).find(bycryptPattern)
if begin == -1:
    print("No encryption based on bcrypt. Exiting...")
    exit(1)
begin+=len(bycryptPattern)
content=str(hex)[begin:]
kdfOptionsLength=content[:8]
content=content[8:]
kdfOptionsLength=int(kdfOptionsLength,16)
kdfPart=content[:kdfOptionsLength*2]
saltLen=int(kdfPart[:8],16)
kdfPart=kdfPart[8:]
salt=re.findall("..",kdfPart[:saltLen*2])
salt=" ".join(salt)
print("Salt:", salt)
kdfPart=kdfPart[saltLen*2:]
rounds=int(kdfPart,16)
print(f"encryption rounds: {rounds} ")
Hannes
  • 307
  • 2
  • 12