129

I'm using the following code to work with Git in a Java application. I have a valid key (use it all the time), and this specific code has work for me before with the same key and git repository, but now I get the following exception:

invalid privatekey: [B@59c40796.

At this line:

jSch.addIdentity("<key_path>/private_key.pem");

My full code:

    String remoteURL = "ssh://git@<git_repository>";
    TransportConfigCallback transportConfigCallback = new SshTransportConfigCallback();
    File gitFolder = new File(workingDirectory);
    if (gitFolder.exists()) FileUtils.delete(gitFolder, FileUtils.RECURSIVE);

    Git git = Git.cloneRepository()
            .setURI(remoteURL)
            .setTransportConfigCallback(transportConfigCallback)
            .setDirectory(new File(workingDirectory))
            .call();
}


private static class SshTransportConfigCallback implements TransportConfigCallback {
    private final SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() {
        @Override
        protected void configure(OpenSshConfig.Host hc, Session session) {
            session.setConfig("StrictHostKeyChecking", "no");
        }

        @Override
        protected JSch createDefaultJSch(FS fs) throws JSchException {
            JSch jSch = super.createDefaultJSch(fs);
            jSch.addIdentity("<key_path>/private_key.pem");

            return jSch;
        }
    };

After searching online, I've change createDefaultJSch to use pemWriter:

@Override
protected JSch createDefaultJSch(FS fs) throws JSchException {
    JSch jSch = super.createDefaultJSch(fs);
    byte[] privateKeyPEM = null;

    try {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");

        List<String> lines = Files.readAllLines(Paths.get("<my_key>.pem"), StandardCharsets.US_ASCII);
        PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(String.join("", lines)));
        RSAPrivateKey privKey = (RSAPrivateKey) keyFactory.generatePrivate(privSpec);

        PKCS8Generator pkcs8 = new PKCS8Generator(privKey);

        StringWriter writer = new StringWriter();
        PemWriter pemWriter = new PemWriter(writer);
        pemWriter.writeObject(pkcs8);

        privateKeyPEM = writer.toString().getBytes("US-ASCII");

    } catch (Exception e) {
        e.printStackTrace();
    }

    jSch.addIdentity("git", privateKeyPEM, null, null);

    return jSch;
}

But still getting "invalid privatekey" exception.

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
o_b7
  • 1,357
  • 2
  • 10
  • 7

12 Answers12

235

Recent versions of OpenSSH (7.8 and newer) generate keys in new OpenSSH format by default, which starts with:

-----BEGIN OPENSSH PRIVATE KEY-----

JSch does not support this key format.


You can use ssh-keygen to convert the key to the classic OpenSSH format:

ssh-keygen -p -f <privateKeyFile> -m pem -P passphrase -N passphrase

This "abuses" -p (change passphrase) command. It will overwrite the private key file identified by the -f option with a new private key in the classic OpenSSH format (pem). You can keep the current passphrase as the new passphrase. If the key was not encrypted with a passphrase, use "" instead of passphrase. After, you can use ssh-keygen -y -e -f <privateKeyFile> >temp.pub to compare public keys and verify the existing public key works with the new format of private key.

For Windows users: Note that ssh-keygen.exe is now built-in in Windows 10/11. And can be downloaded from Microsoft Win32-OpenSSH project for older versions of Windows.


On Windows, you can also use PuTTYgen (from PuTTY package):

  • Start PuTTYgen
  • Load the key
  • Go to Conversions > Export OpenSSH key.
    For RSA keys, it will use the classic format.

If you are creating a new key with ssh-keygen, just add -m PEM to generate the new key in the classic format:

ssh-keygen -m PEM

Actually, the original JSch does not seem to be actively maintained anymore. So particularly, if you are starting new project, it might not be the best library to begin with. JSch suffers from many compatibility problems nowadays. For some important ones, see:

Instead, you might consider using this JSch fork:
https://github.com/mwiede/jsch

Among other, it does support the new OpenSSH key format.

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
  • 33
    extra kudos for pointing out how to convert an existing key instead of just generating a new one – sryll Mar 23 '20 at 13:25
148

I also stumbled upon this issue. running Jgit on mac, for some users we saw the following exception:

org.eclipse.jgit.transport.JschConfigSessionFactory.getSession(JschConfigSessionFactory.java:160)
    at org.eclipse.jgit.transport.SshTransport.getSession(SshTransport.java:137)
    at org.eclipse.jgit.transport.TransportGitSsh$SshFetchConnection.<init>(TransportGitSsh.java:274)
    at org.eclipse.jgit.transport.TransportGitSsh.openFetch(TransportGitSsh.java:169)
    at org.eclipse.jgit.transport.FetchProcess.executeImp(FetchProcess.java:136)
    at org.eclipse.jgit.transport.FetchProcess.execute(FetchProcess.java:122)
    at org.eclipse.jgit.transport.Transport.fetch(Transport.java:1236)
    at org.eclipse.jgit.api.FetchCommand.call(FetchCommand.java:234)
    ... 17 more
Caused by: com.jcraft.jsch.JSchException: invalid privatekey: [B@e4487af
    at com.jcraft.jsch.KeyPair.load(KeyPair.java:664)
    at com.jcraft.jsch.KeyPair.load(KeyPair.java:561)
    at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
    at com.jcraft.jsch.JSch.addIdentity(JSch.java:407)
    at com.jcraft.jsch.JSch.addIdentity(JSch.java:367)
    at org.eclipse.jgit.transport.JschConfigSessionFactory.getJSch(JschConfigSessionFactory.java:276)
    at org.eclipse.jgit.transport.JschConfigSessionFactory.createSession(JschConfigSessionFactory.java:220)
    at org.eclipse.jgit.transport.JschConfigSessionFactory.createSession(JschConfigSessionFactory.java:176)
    at org.eclipse.jgit.transport.JschConfigSessionFactory.getSession(JschConfigSessionFactory.java:110)

The root cause was discovered to be the ssh private key mismatch. The exception only happened for users with key of newer kind ed25519, which outputs this key header:

-----BEGIN OPENSSH PRIVATE KEY-----

instead of kind RSA:

-----BEGIN RSA PRIVATE KEY-----

regenerating an RSA key (ssh-keygen -t rsa), made the exception go away.

Edit following comments: If you have OpenSSH 7.8 and above you might need to add -m PEM to the generation command: ssh-keygen -t rsa -m PEM

Natan
  • 1,944
  • 1
  • 11
  • 16
  • 3
    Additionally, JSch seems to read `~/ssh/config` and fails if any non-RSA and/or non-PEM files are added to the list via `IdentityFile` directive. – Bass Jul 27 '20 at 21:34
17

Instead of converting the OPENSSH key format to the format, which original JSch supports, you can also switch to a fork of JSch, which you can find at https://github.com/mwiede/jsch

Your only need to replace your JSch Maven coordinates with com.github.mwiede:jsch:0.1.61.

The fork does support the OPENSSH key format and several more algorithms, which might become important in the future, as OpenSSH servers will restrict the allowed sets of algorithms to the most secure ones.

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Matthias Wiedemann
  • 1,313
  • 12
  • 22
8

Quite late to reply, but want to leave track of how to face the issue.

The point, as many people already mentioned in their answers, is actually the way you generate the key and with the -m PEM option resolves.

However if as it just happened to me, you could not regenerate the key because the public part had already been installed in several servers, you can still convert your private key to a suitable format.

To do so, just issue the following command:

ssh-keygen -p -m pem -f id_rsa

It will ask for input of a new passphrase. With parameters -P (old passphrase) and -N (new passphrase) you can provide them at once, if needed.

Akshay Hiremath
  • 950
  • 2
  • 12
  • 34
Stefano Cazzola
  • 1,597
  • 1
  • 20
  • 36
3

JSch does not support this key format. It supports only RSAPrivateKey. This command works for me. Try this solution

ssh-keygen -m PEM -t rsa -b 2048

//edited to rsa with 2048 keysize

AirUp
  • 426
  • 1
  • 8
  • 18
1
  1. You read a file named .pem and de-base64 all of it and treat the result as PKCS8-unencrypted, apparently successfully. This means the file was NOT PEM-format. PEM format at minimum MUST have the dash-BEGIN and dash-END lines to be valid, which if not removed cause de-base64 to either fail or be wrong. (Some PEM formats also have 822-style headers which must be handled.)

  2. You appear to be using BouncyCastle, but in my versions there is no PKCS8Generator constructor that takes only RSAPrivateKey. The closest thing that works is JcaPKCS8Generator (RSAPrivateKey implements PrivateKey, OutputEncryptor=null) (i.e. a different but related class, and two arguments not one).

  3. PemWriter is buffered, and you didn't flush it before looking at the underlying StringWriter. As a result writer.toString().getBytes() is an empty/zero-length array, which JSch rightly considers invalid.

With #2 and #3 fixed and using my input, and calling JSch directly instead of via JGit, it works for me.

dave_thompson_085
  • 34,712
  • 6
  • 50
  • 70
1

Besides issues with the format of the Private Key, this error "JSchException: invalid privatekey" can also occur when you:

The reason for this is an if statement from the JSch source code, class KeyPair, method parseHeader at line 803: if (buf[i] == 0x0d) {...}: https://github.com/is/jsch/blob/master/src/main/java/com/jcraft/jsch/KeyPair.java#L803

Because of this if statement, are taken into consideration only the newline characters encoded like \r (0x0D == 13) (applicable to Windows and MacOS). But UNIX uses \n (0x0A == 10). Encodings are explained in this thread, for example: What are the differences between char literals '\n' and '\r' in Java?


So if your Private Key has the correct structure, but you run the app from Linux (or any other UNIX OS), then the Byte Array that corresponds to the content of your Private Key will be different based on the Operating System from which you run your app.

This is an example where the Byte Arrays have different contents:

  • When app runs on Linux: [114, 115, 97, 10, 69, 110] => 10 is the "\n"
  • When app runs on Windows: [114, 115, 97, 13, 10, 69, 110] => 13 10 is the "\r\n"

This image illustrates the different contents of the Byte Arrays converted back as Strings , when the app is run as a WAR file from Linux and Windows, captured through remote debugging. I used the latest version of JSch that is currently available: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55


Therefore, if you run your app from Linux, then a solution would be to:

1. Get the InputStream from your Private and Public Keys (the files should be added to the "resources" directory of your Spring project):

InputStream privateKeyInputStream = new ClassPathResource("private-key.ppk").getInputStream();
InputStream publicKeyInputStream = new ClassPathResource("public-key.ppk").getInputStream();

2. Convert the InputStream to ByteArrays

byte[] privateKeyAsByteArray = IOUtils.toByteArray(privateKeyInputStream);
byte[] publicKeyAsByteArray = IOUtils.toByteArray(publicKeyInputStream);

3. Fix the encoding, by replacing the bytes of 10 (0x0A) with bytes of 13 (0x0D), before calling the addIdentity method from JSch:

for (int i = 0; i < privateKeyAsByteArray.length; i++) {
    if (privateKeyAsByteArray[i] == 10) {   // if current element is a 10 (\n) (UNIX)
        privateKeyAsByteArray[i] = 13;      // replace it with 13 (\r) (a byte that can be interpreted)
    }
}

for (int i = 0; i < publicKeyAsByteArray.length; i++) {
    if (publicKeyAsByteArray[i] == 10) {    // if current element is a 10 (\n) (UNIX)
        publicKeyAsByteArray[i] = 13;       // replace it with 13 (\r) (a byte that can be interpreted)
    }
}

4. Call the addIdentity method:

jSch.addIdentity("private-key.ppk", privateKeyAsByteArray, publicKeyAsByteArray, passphraseAsString.getBytes());

I was thinking that this scenario could help someone who faces this error, until that if statement from the source code of the JSch library is updated to support also UNIX characters.

  • The original JSch library seems to be abandoned. But this GihHub project continues to add improvements and updates to the original implementation. From version 0.2.7 upwards, this problem with the `invalid private-key for UNIX runs` was solved: https://github.com/mwiede/jsch – Pop Alexandru May 18 '23 at 10:44
0

So have the same problem fix it by PuTTYgen

enter image description here

1)Open PuTTYgen

  1. Press "Load" and choose the privateKey

enter image description here

3)press now on "Conversions" -> and choose the first option "Export OPENSSH key"

enter image description here

  1. save it as file (not need any format) and use it
Vladi
  • 1,662
  • 19
  • 30
0

From my sides, I meet the same issue. In my host file, there has this configuration below, which makes jsch reading ssh config by id_ed25519. You should use id_rsa.

   Host *
     AddKeysToAgent yes
     IdentityFile /Users/xxx/.ssh/id_ed25519
0

Since JSCH is no longer maintained and doesn't support most of the latest OpenSSH key algorithms, JGIT has now turned into Apache MINA SSHD for SSH connectivity. For this JGIT also provides the artifact org.eclipse.jgit.ssh.apache . To use this all you have to do is switch the dependency from JGit jsch artifact to JGit ssh.apache artifact and set a new SshdSessionFactory instance (which is apache implementation of SshSessionFactory) to org.eclipse.jgit.transport.SshTransport.

Switch dependency;

From :

<dependency>
        <groupId>org.eclipse.jgit</groupId>
        <artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
</dependency>

To :

<dependency>
        <groupId>org.eclipse.jgit</groupId>
        <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
</dependency>

Setting SshdSessionFactory for a private key with passphrase;

TransportCommand<T, C> transportCommand = <Any Transport command in JGIT>;
File sshDir = new File(FS.DETECTED.userHome(), File.separator+SSH_DIR);
      SshdSessionFactory sshSessionFactory = new SshdSessionFactoryBuilder().setPreferredAuthentications("publickey")
        .setHomeDirectory(FS.DETECTED.userHome()).setSshDirectory(sshDir)
        .setKeyPasswordProvider(cp -> new IdentityPasswordProvider(cp)
        {
          @Override
          protected char[] getPassword(URIish uri, String message)
          {
            return passphrase.toCharArray();
          }
        }).build(null);
 transportCommand.setTransportConfigCallback(transport -> ((SshTransport) transport).setSshSessionFactory(sshSessionFactory));
Priyal
  • 444
  • 5
  • 18
0

Try using this dependency in your maven pom.xml and ensure that you check from which JSH class dependency is getting used in your code . Sometimes older dependency gets picked . Here using below dependency even OPEN SSH KEY will work fine

<dependency>
            <groupId>com.github.mwiede</groupId>
            <artifactId>jsch</artifactId>
            <version>0.2.9</version>
        </dependency>
-2

I wanted to add that in order to avoid the below headers you need to create the key with

-C "any-comment"

Headers that will be removed from the private Key:

Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,3551DFC375229D5758289E8D366082FE

Leaving only

 -----BEGIN RSA PRIVATE KEY-----
YOUR_KEY_HERE
-----END RSA PRIVATE KEY-----
Ferlorin
  • 50
  • 3
  • That's not correct. Those headers indicate that the key is encrypted with a passphrase. They have nothing to do with `-C`. – Martin Prikryl Dec 14 '21 at 12:26
  • for some reason in my configuration the same command with the `-C` returned the file without the headers. The RSA key did not work if I removed them manually, I had to use the `-C` – Ferlorin Dec 14 '21 at 12:34
  • You have probably entered a passphrase when prompted in one case, while not in the other. – Martin Prikryl Dec 14 '21 at 12:43