/// <summary> /// Read private key parameters with OpenSSH format. /// </summary> /// <param name="passphrase">passphrase for decrypt the key file</param> /// <param name="keyPair">key pair</param> /// <param name="comment">comment or empty if it didn't exist</param> public void Load(string passphrase, out KeyPair keyPair, out string comment) { // Note: // The structure of the private key format is described in "PROTOCOL.key" file in OpenSSH sources. String base64Text; using (StreamReader sreader = GetStreamReader()) { string line = sreader.ReadLine(); if (line == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (unexpected eof)"); } if (line != PrivateKeyFileHeader.SSH2_OPENSSH_HEADER_OPENSSH) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (unexpected key type)"); } string footer = line.Replace("BEGIN", "END"); StringBuilder buf = new StringBuilder(); while (true) { line = sreader.ReadLine(); if (line == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (unexpected eof)"); } if (line == footer) { break; } buf.Append(line); } base64Text = buf.ToString(); } byte[] blob = Base64.Decode(Encoding.ASCII.GetBytes(base64Text)); int numKeys; // number of keys byte[][] publicKeyBlobs; byte[] privateKeyData; bool decrypted; using (var blobStream = new MemoryStream(blob, false)) { if (!CheckMagic(blobStream)) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (unsupported format)"); } string cipherName = ReadString(blobStream); CipherAlgorithm?cipherAlgorithm; int cipherKeySize; int cipherIVSize; switch (cipherName) { case "none": cipherAlgorithm = null; cipherKeySize = 0; cipherIVSize = 0; break; case "aes128-cbc": cipherAlgorithm = CipherAlgorithm.AES128; cipherKeySize = 16; cipherIVSize = 16; // use block size break; case "aes192-cbc": cipherAlgorithm = CipherAlgorithm.AES192; cipherKeySize = 24; cipherIVSize = 16; // use block size break; case "aes256-cbc": cipherAlgorithm = CipherAlgorithm.AES256; cipherKeySize = 32; cipherIVSize = 16; // use block size break; default: throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (unsupported cipher)"); } string kdfName = ReadString(blobStream); if (kdfName != "bcrypt" && kdfName != "none") { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (unsupported kdf)"); } if ((cipherName == "none") != (kdfName == "none")) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid cipher)"); } byte[] kdfOptions = ReadBytes(blobStream); if (kdfOptions == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (missing kdf options)"); } byte[] kdfSalt; uint kdfRounds; byte[] key; byte[] iv; if (kdfName == "none") { kdfSalt = null; kdfRounds = 0; key = null; iv = null; } else { if (!ReadKdfOptions(kdfOptions, out kdfSalt, out kdfRounds)) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid kdf options)"); } if (passphrase == null || passphrase.Length == 0) { throw new SSHException(Strings.GetString("WrongPassphrase")); } // prepare decryption Bcrypt bcrypt = new Bcrypt(); byte[] tmpkey = bcrypt.BcryptPbkdf(passphrase, kdfSalt, kdfRounds, cipherKeySize + cipherIVSize); if (tmpkey == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid kdf options)"); } key = new byte[cipherKeySize]; Buffer.BlockCopy(tmpkey, 0, key, 0, cipherKeySize); iv = new byte[cipherIVSize]; Buffer.BlockCopy(tmpkey, cipherKeySize, iv, 0, cipherIVSize); } if (!ReadInt32(blobStream, out numKeys) || numKeys < 0) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (missing keys)"); } publicKeyBlobs = new byte[numKeys][]; for (int i = 0; i < numKeys; ++i) { byte[] data = ReadBytes(blobStream); if (data == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (missing public keys)"); } publicKeyBlobs[i] = data; } privateKeyData = ReadBytes(blobStream); if (privateKeyData == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (missing private keys)"); } if (cipherAlgorithm.HasValue && key != null && iv != null) { // decrypt private keys Cipher cipher = CipherFactory.CreateCipher(SSHProtocol.SSH2, cipherAlgorithm.Value, key, iv); cipher.Decrypt(privateKeyData, 0, privateKeyData.Length, privateKeyData, 0); decrypted = true; } else { decrypted = false; } } using (var privateKeysStream = new MemoryStream(privateKeyData, false)) { uint check1, check2; if (!ReadUInt32(privateKeysStream, out check1) || !ReadUInt32(privateKeysStream, out check2)) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid private keys)"); } if (check1 != check2) { throw new SSHException(decrypted ? Strings.GetString("WrongPassphrase") : Strings.GetString("NotValidPrivateKeyFile")); } for (int i = 0; i < numKeys; ++i) { string privateKeyType = ReadString(privateKeysStream); using (var publicKeyBlobStream = new MemoryStream(publicKeyBlobs[i], false)) { string publicKeyType = ReadString(publicKeyBlobStream); if (publicKeyType != privateKeyType) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (key type unmatched)"); } switch (privateKeyType) { case "ssh-ed25519": { byte[] pk = ReadBytes(privateKeysStream); if (pk == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile")); } byte[] sk = ReadBytes(privateKeysStream); if (sk == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile")); } string cmnt = ReadString(privateKeysStream); // comment if (cmnt == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile")); } byte[] publicKey = ReadBytes(publicKeyBlobStream); if (publicKey == null) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile")); } // sanity check if (!AreEqual(publicKey, pk)) { throw new SSHException(Strings.GetString("WrongPassphrase")); } // first 32 bytes of secret key is used as a private key for ed25519 byte[] privateKey = new byte[32]; if (sk.Length < privateKey.Length) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile")); } Buffer.BlockCopy(sk, 0, privateKey, 0, privateKey.Length); var curve = EdwardsCurve.FindByAlgorithm(PublicKeyAlgorithm.ED25519); if (curve != null) { var kp = new EDDSAKeyPair(curve, new EDDSAPublicKey(curve, publicKey), privateKey); if (!kp.CheckKeyConsistency()) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile")); } keyPair = kp; comment = cmnt; return; } } break; default: // unsupported key type; check the next key. break; } } } } throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (supported private key was not found)"); }
/// <summary> /// Read PuTTY SSH2 private key parameters. /// </summary> /// <param name="passphrase">passphrase for decrypt the key file</param> /// <param name="keyPair">key pair</param> /// <param name="comment">comment or empty if it didn't exist</param> public void Load(string passphrase, out KeyPair keyPair, out string comment) { if (keyFile == null) { throw new SSHException("A key file is not loaded yet"); } int version; string keyTypeName; KeyType keyType; string encryptionName; CipherAlgorithm?encryption; byte[] publicBlob; byte[] privateBlob; string privateMac; string privateHash; using (StreamReader sreader = GetStreamReader()) { //*** Read header and key type ReadHeaderLine(sreader, out version, out keyTypeName); if (keyTypeName == "ssh-rsa") { keyType = KeyType.RSA; } else if (keyTypeName == "ssh-dss") { keyType = KeyType.DSA; } else if (keyTypeName.StartsWith("ecdsa-sha2-")) { keyType = KeyType.ECDSA; } else if (keyTypeName == "ssh-ed25519") { keyType = KeyType.ED25519; } else { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (unexpected key type)"); } //*** Read encryption ReadItemLine(sreader, "Encryption", out encryptionName); if (encryptionName == "aes256-cbc") { encryption = CipherAlgorithm.AES256; } else if (encryptionName == "none") { encryption = null; passphrase = ""; // prevent HMAC error } else { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (unexpected encryption)"); } //*** Read comment ReadItemLine(sreader, "Comment", out comment); //*** Read public lines string publicLinesStr; ReadItemLine(sreader, "Public-Lines", out publicLinesStr); int publicLines; if (!Int32.TryParse(publicLinesStr, out publicLines) || publicLines < 0) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid public lines)"); } ReadBlob(sreader, publicLines, out publicBlob); //*** Read private lines string privateLinesStr; ReadItemLine(sreader, "Private-Lines", out privateLinesStr); int privateLines; if (!Int32.TryParse(privateLinesStr, out privateLines) || privateLines < 0) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid private lines)"); } ReadBlob(sreader, privateLines, out privateBlob); //*** Read private MAC ReadPrivateMACLine(sreader, version, out privateMac, out privateHash); } if (encryption.HasValue) { byte[] key = PuTTYPassphraseToKey(passphrase); byte[] iv = new byte[16]; Cipher cipher = CipherFactory.CreateCipher(SSHProtocol.SSH2, encryption.Value, key, iv); if (privateBlob.Length % cipher.BlockSize != 0) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid key data size)"); } cipher.Decrypt(privateBlob, 0, privateBlob.Length, privateBlob, 0); } bool verified = Verify(version, privateMac, privateHash, passphrase, keyTypeName, encryptionName, comment, publicBlob, privateBlob); if (!verified) { if (encryption.HasValue) { throw new SSHException(Strings.GetString("WrongPassphrase")); } else { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (HMAC verification failed)"); } } if (keyType == KeyType.RSA) { SSH2DataReader reader = new SSH2DataReader(publicBlob); string magic = reader.ReadString(); if (magic != "ssh-rsa") { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (missing magic)"); } BigInteger e = reader.ReadMPInt(); BigInteger n = reader.ReadMPInt(); reader = new SSH2DataReader(privateBlob); BigInteger d = reader.ReadMPInt(); BigInteger p = reader.ReadMPInt(); BigInteger q = reader.ReadMPInt(); BigInteger iqmp = reader.ReadMPInt(); BigInteger u = p.ModInverse(q); keyPair = new RSAKeyPair(e, d, n, u, p, q); } else if (keyType == KeyType.DSA) { SSH2DataReader reader = new SSH2DataReader(publicBlob); string magic = reader.ReadString(); if (magic != "ssh-dss") { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (missing magic)"); } BigInteger p = reader.ReadMPInt(); BigInteger q = reader.ReadMPInt(); BigInteger g = reader.ReadMPInt(); BigInteger y = reader.ReadMPInt(); reader = new SSH2DataReader(privateBlob); BigInteger x = reader.ReadMPInt(); keyPair = new DSAKeyPair(p, g, q, y, x); } else if (keyType == KeyType.ECDSA) { SSH2DataReader reader = new SSH2DataReader(publicBlob); string algorithmName = reader.ReadString(); string curveName = reader.ReadString(); byte[] publicKeyPt = reader.ReadByteString(); reader = new SSH2DataReader(privateBlob); BigInteger privateKey = reader.ReadMPInt(); EllipticCurve curve = EllipticCurve.FindByName(curveName); if (curve == null) { throw new SSHException(Strings.GetString("UnsupportedEllipticCurve") + " : " + curveName); } ECPoint publicKey; if (!ECPoint.Parse(publicKeyPt, curve, out publicKey)) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (parsing public key failed)"); } keyPair = new ECDSAKeyPair(curve, new ECDSAPublicKey(curve, publicKey), privateKey); if (!((ECDSAKeyPair)keyPair).CheckKeyConsistency()) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid key pair)"); } } else if (keyType == KeyType.ED25519) { SSH2DataReader reader = new SSH2DataReader(publicBlob); string algorithmName = reader.ReadString(); byte[] publicKey = reader.ReadByteString(); reader = new SSH2DataReader(privateBlob); byte[] privateKey = reader.ReadByteString(); EdwardsCurve curve = EdwardsCurve.FindByAlgorithm(PublicKeyAlgorithm.ED25519); if (curve == null) { throw new SSHException(Strings.GetString("UnsupportedEllipticCurve")); } keyPair = new EDDSAKeyPair(curve, new EDDSAPublicKey(curve, publicKey), privateKey); if (!((EDDSAKeyPair)keyPair).CheckKeyConsistency()) { throw new SSHException(Strings.GetString("NotValidPrivateKeyFile") + " (invalid key pair)"); } } else { throw new SSHException("Unknown file type. This should not happen."); } }