Skip to content

Commit 6d6d530

Browse files
author
Alain Knaff
committed
Also support ED25519 ssh keys
1 parent c4e6b16 commit 6d6d530

7 files changed

Lines changed: 111 additions & 40 deletions

File tree

app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ dependencies {
7171
//compile 'commons-net:commons-net:+'
7272
// https://mvnrepository.com/artifact/it.sauronsoftware/ftp4j
7373
implementation 'com.github.mwiede:jsch:2.27.7'
74+
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
75+
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
7476

7577
implementation 'com.google.android.material:material:1.13.0'
7678
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'

app/lint.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<lint>
3+
<issue id="InvalidPackage" severity="ignore" />
4+
<!-- for bouncycastle
5+
https://codingtechroom.com/question/trouble-integrating-bouncycastle-jar
6+
-->
7+
38
<issue id="LogConditional" severity="ignore" />
49
<!-- to be fixed once we can do non-debug builds -->
510

app/proguard-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@
1919
# If you keep the line number information, uncomment this to
2020
# hide the original source file name.
2121
#-renamesourcefileattribute SourceFile
22+
-keep class org.bouncycastle.jcajce.provider.** { *; }
23+
-keep class org.bouncycastle.jce.provider.** { *; }

app/src/main/java/com/island/androidsftpdocumentsprovider/MainActivity.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,23 @@ class MainActivity : ProviderActivity()
114114
}
115115
}
116116

117+
val keyTypes = arrayOf("RSA",
118+
"ED25519",
119+
// "ECDSA"
120+
);
121+
117122
private fun generateKey2(view: View) {
118-
Keygen.genKey(this);
123+
AlertDialog.Builder(this)
124+
.setTitle("Choose a key type")
125+
.setSingleChoiceItems(keyTypes, 1) {
126+
dialog, which -> generateKey3(keyTypes[which], view);
127+
dialog.dismiss();
128+
}
129+
.show();
130+
}
131+
132+
private fun generateKey3(algo: String, view: View) {
133+
Keygen.genKey(this, algo);
119134
fixButtonState();
120135
}
121136

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.island.sftp;
2+
3+
import java.security.Security;
4+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
5+
6+
/**
7+
* Reload sufficiently recent BouncyCastle library
8+
*/
9+
public abstract class BouncyCastle {
10+
11+
static {
12+
// https://stackoverflow.com/questions/2584401/how-to-add-bouncy-castle-algorithm-to-android
13+
// By default Android _has_ bouncy castle, but an old version. Remove
14+
// that first
15+
//
16+
Security.removeProvider("BC");
17+
18+
Security.insertProviderAt(new BouncyCastleProvider(), 1);
19+
}
20+
21+
/**
22+
* Trigger method does nothing by itself, but loading the class and its
23+
* BouncyCastle provider.
24+
*/
25+
public static void trigger() {
26+
}
27+
}

app/src/main/java/com/island/sftp/Keygen.java

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@
44
import java.io.BufferedReader;
55
import java.io.File;
66
import java.io.PrintWriter;
7+
import java.io.OutputStreamWriter;
78
import java.io.FileNotFoundException;
89
import java.io.IOException;
910
import java.security.Key;
1011
import java.security.KeyPair;
1112
import java.security.KeyPairGenerator;
1213
import java.security.NoSuchAlgorithmException;
1314
import java.security.PublicKey;
15+
import java.security.NoSuchProviderException;
16+
17+
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
18+
import org.bouncycastle.crypto.util.OpenSSHPrivateKeyUtil;
19+
import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil;
20+
import org.bouncycastle.crypto.util.PrivateKeyFactory;
21+
import org.bouncycastle.crypto.util.PublicKeyFactory;
1422

1523
/* This file is part of SFTP-SAF, an Android app to access sftp servers via Storage access framework
1624
Copyright (C) 2025,2026 Alain Knaff
@@ -22,8 +30,6 @@
2230
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
2331
*/
2432

25-
import java.security.interfaces.RSAPublicKey;
26-
import java.nio.charset.StandardCharsets;
2733
import java.nio.ByteOrder;
2834
import java.nio.ByteBuffer;
2935

@@ -43,62 +49,75 @@ public class Keygen {
4349
public static final String PRIVATE_KEY_FILE="privateKey.pem";
4450
public static final String PUBLIC_KEY_FILE="publicKey.txt";
4551

46-
public static void genKey(Context ctx) {
52+
public static void genKey(Context ctx, String algo) {
4753
try {
48-
// confirmation dialog if it already exists:
49-
// https://stackoverflow.com/questions/5127407/how-to-implement-a-confirmation-yes-no-dialogpreference
54+
BouncyCastle.trigger();
5055

51-
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
52-
keyGen.initialize(2048);
56+
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algo,"BC");
5357
KeyPair keyPair = keyGen.generateKeyPair();
54-
saveKeyToFile(ctx, PRIVATE_KEY_FILE, convertToPEM(keyPair.getPrivate(), "PRIVATE"));
55-
saveKeyToFile(ctx, PUBLIC_KEY_FILE, getEncodedSshPublicKey( (PublicKey) keyPair.getPublic()));
5658

59+
saveKeyToFile(ctx, PRIVATE_KEY_FILE,
60+
convertToPEM(keyPair.getPrivate(),
61+
"RSA".equals(algo)));
62+
63+
saveKeyToFile(ctx, PUBLIC_KEY_FILE,
64+
getEncodedSshPublicKey(keyPair.getPublic()));
65+
} catch (IOException e) {
66+
Log.e(TAG, "Error generating keys: " + e.getMessage());
67+
} catch (NoSuchProviderException e) {
68+
Log.e(TAG, "Error generating keys: " + e.getMessage());
5769
} catch (NoSuchAlgorithmException e) {
5870
Log.e(TAG, "Error generating keys: " + e.getMessage());
5971
}
6072
}
6173

62-
private static void saveKeyToFile(Context ctx, String fileName, String key) {
74+
private static void saveKeyToFile(Context ctx, String fileName, String key)
75+
throws IOException
76+
{
6377
try(PrintWriter pw = new PrintWriter(ctx.openFileOutput(fileName, 0))) {
6478
pw.println(key);
6579
System.out.println(fileName + " saved successfully.");
6680
} catch (IOException e) {
6781
Log.e(TAG, "Error saving key to file: " + e.getMessage());
82+
throw e;
6883
}
6984
}
7085

7186
private static String toBase64(byte[] bin) {
7287
return Base64.encodeToString(bin, Base64.NO_WRAP);
7388
}
7489

75-
public static String convertToPEM(Key key, String type) {
76-
byte[] encodedKey = key.getEncoded();
90+
public static String convertToPEM(Key key, boolean isRsa)
91+
throws IOException
92+
{
93+
byte[] encodedKey;
94+
String type;
95+
if(isRsa) {
96+
encodedKey = key.getEncoded();
97+
type = "";
98+
} else {
99+
AsymmetricKeyParameter bprv =
100+
PrivateKeyFactory.createKey(key.getEncoded());
101+
encodedKey = OpenSSHPrivateKeyUtil.encodePrivateKey(bprv);
102+
type="OPENSSH ";
103+
}
77104
String base64Key = toBase64(encodedKey);
78-
return "-----BEGIN "+type+" KEY-----\n" + base64Key + "\n-----END "+type+" KEY-----";
105+
return "-----BEGIN "+type+"PRIVATE KEY-----\n" + base64Key + "\n-----END "+type+"PRIVATE KEY-----";
79106
}
80107

81108
// see https://linuxtut.com/en/ee3c7d0ba7d4610a9d21/ for
82109
// outputting public key
83-
public static String getEncodedSshPublicKey(final PublicKey pKey) {
84-
final String sig = "ssh-rsa";
85-
86-
RSAPublicKey publicKey = (RSAPublicKey) pKey;
87-
final byte[] sigBytes = sig.getBytes(StandardCharsets.US_ASCII);
88-
final byte[] eBytes = publicKey.getPublicExponent().toByteArray();
89-
final byte[] nBytes = publicKey.getModulus().toByteArray();
90-
91-
final int size = 4 + sigBytes.length
92-
+ 4 + eBytes.length
93-
+ 4 + nBytes.length;
94-
95-
final byte[] publicKeyBytes = ByteBuffer.allocate(size)
96-
.order(ByteOrder.BIG_ENDIAN)
97-
.putInt(sigBytes.length).put(sigBytes)
98-
.putInt(eBytes.length).put(eBytes)
99-
.putInt(nBytes.length).put(nBytes)
100-
.array();
101-
110+
public static String getEncodedSshPublicKey(final PublicKey pKey)
111+
throws IOException
112+
{
113+
AsymmetricKeyParameter bpub =
114+
PublicKeyFactory.createKey(pKey.getEncoded());
115+
byte[] publicKeyBytes = OpenSSHPublicKeyUtil.encodePublicKey(bpub);
116+
int sigLen = ByteBuffer
117+
.wrap(publicKeyBytes)
118+
.order(ByteOrder.BIG_ENDIAN)
119+
.getInt();
120+
final String sig = new String(publicKeyBytes, 4, sigLen);
102121
final String publicKeyBase64 = toBase64(publicKeyBytes);
103122

104123
final String publicKeyEncoded = sig + " " + publicKeyBase64 + " user@sftpprovider";

app/src/main/java/com/island/sftp/SFTP.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,13 @@ public static Uri parseUri(String name) {
6767
return Uri.parse(SFTP.SCHEME+name);
6868
}
6969

70-
public SFTP(Context ctx, Uri uri, String password, int id)
71-
throws ConnectException
72-
{
73-
init(ctx, uri, password);
74-
this.id = id;
75-
}
70+
public SFTP(Context ctx, Uri uri, String password, int id)
71+
throws ConnectException
72+
{
73+
BouncyCastle.trigger();
74+
init(ctx, uri, password);
75+
this.id = id;
76+
}
7677

7778
public int getId() {
7879
return id;
@@ -208,7 +209,7 @@ public synchronized void close() throws IOException
208209
"Could not read "+link.getPath());
209210
continue;
210211
}
211-
}
212+
}
212213
file=new SftpFile(directory, entry.getFilename(), attrs);
213214
files.add(file);
214215
lastModified.put(file,file.getSftpLastModified());

0 commit comments

Comments
 (0)