|
~ To be, or not to be, or to ask someone to be. ~ null-i.net |
| AndroidJava/sshクライアントをApacheMINAで | |
|
Apache-MINA を Android で動かす(2026-04-18) ssh でサーバに接続して echo 'hello world' を実行する例です。 ※JSch で実装する方法はこちら -> sshクライアントをJSchで import android.content.Context;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.auth.keyboard.UserInteraction;
import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.security.KeyPair;
import java.security.Security;
import java.util.Collection;
import java.util.EnumSet;
public class MySsh02 {
private static final String user = "your_user";
private static final String pass_word = "your_password";
//private static final String pass_phrase = "for_client_key";
private static final String host = "your.remote.server";
private static final int port = 22; // you should change from default(22)!
private static final String pass_phrase = "test"; // cl_private_key のパスフレーズ
private static final String cl_private_key = "-----BEGIN EC PRIVATE KEY-----\n" +
"Proc-Type: 4,ENCRYPTED\n" +
"DEK-Info: AES-256-CBC,0D2B70580F6876EEAF5E024E680F72D7\n" +
"\n" +
"wn5Cup9EGYSwJDxtZXahXyyYUJ9W5jR/SuZf9/peBi9ycJ6zZCvTT/+5EItmZ53f\n" +
"5eCN2NjN+CvG/nj+Y3KycfQK8lSVKZWzW8RnPL66PhZzMd8OA3JVSZZmOCrBLNEt\n" +
"K84EBLcsuAnIQ5kOpDlp3LK4NZarHSdQP6Kwj5Hehfw=\n" +
"-----END EC PRIVATE KEY-----\n";
// サーバに置く公開鍵の例(OpenSSH形式、ECDSA)
// ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKLiIIQ4Or7MvZgPUGIrhiQp8cEE/uj9KgR+bss2hBCqKjF0ZKTwcURSMyyuBbKnvccNTE2RtANg7KbExg6s0Lk= ecdsa
private SshClient sshClient;
private ClientSession clientSession;
private ClientChannel clientChannel;
MyUserInteraction userInteraction = new MyUserInteraction();
MySsh02(Context context){
// Android内で使われている BC(=BouncyCastle)を上書きする必要がある
// これが無いとsshClient.start() で IllegalArgumentException とか出すこともある
// https://github.com/corda/mina-sshd-r3/blob/master/docs/android.md
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); // プリセット分を除外して、差し替える
}
Security.addProvider(new BouncyCastleProvider()); // とにかく足す!
// Apache MINA sshd の以下のエラーに対する対応。
// Caused by: java.lang.IllegalArgumentException:
// No user home folder available. You should call
// org.apache.sshd.common.util.io.PathUtils.setUserHomeFolderResolver() method
// to set user home folder as there is no home folder on Android
// -> Android doesn't set the "user.home" property by default.
//
// 回答として、https://github.com/apache/mina-sshd/tree/master/docs
// The NativeFileSystemFactory auto-detects this folder for standard O/S,
// but for Android one needs to call its setUsersHomeDir method explicitly
// - or extend it and override getUserHomeDir method.
//
// なので、エラー回避のために user.home を仮設定する
System.setProperty("user.home", context.getApplicationInfo().dataDir);
// この中で呼ばれる Collections.unmodifiableNavigableSet() が
// API26 (Android8) 以降でなければ未対応
// (2025.08 現在では targetCompatibility JavaVersion.VERSION_17 にしても回避不可能)
sshClient = SshClient.setUpDefaultClient();
}
private boolean isRun;
// とりあえずログは全部 StringBuffer に入れておく
private final StringBuffer buffer = new StringBuffer();
void log(String s){
System.out.println(s);
if(buffer.length()>0) buffer.append("\n");
buffer.append(s);
}
String getLog(){
return buffer.toString();
}
// Android ではネットワーク関連の処理は
// メインスレッドとは別スレッドで動かす必要がある
void run(){
if(isRun) return;
isRun = true;
new Thread(new Runnable() {
@Override
public void run() {
try {
sshThread();
}catch (Exception e){
log("Exception:" + e + ", cause:" + e.getCause());
e.printStackTrace();
}
isRun = false;
}
}).start();
}
// ssh 接続から、認証、切断までの例
private void sshThread(){
try {
log("start ssh");
buffer.setLength(0);
// DSA は duplicated なので、どうしても必要なら追加(ただしセキュリティには注意)
//List<NamedFactory<Signature>> signatureFactories = sshClient.getSignatureFactories();
//List<BuiltinSignatures> signatures2 = new ArrayList<>();
//signatures2.add(BuiltinSignatures.dsa); // DSA は duplicated なので追加が必要
//signatures2.add(BuiltinSignatures.dsa_cert); // DSA は duplicated なので追加が必要
//signatureFactories.addAll(NamedFactory.setUpBuiltinFactories(false, signatures2));
// サーバ側の公開鍵(フィンガープリント)を検証する場合は
// ServerKeyVerifier verifier = new ServerKeyVerifier() {
// @Override
// public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
// String f_print = KeyUtils.getFingerPrint(serverKey);
// log("finger print:" + f_print);
// return true;
// // 検証して、問題なければ return true;
// // 各情報は保存して、次回以降の検証で使う
// }
// };
// sshClient.setServerKeyVerifier(verifier); // サーバ鍵の確認を有効化
sshClient.setUserInteraction(userInteraction);
log("start");
sshClient.start();
clientSession = sshClient.connect(user, host, port).verify().getClientSession();
// こうやって直接指定するか、userInteraction 内でpass_wordを返すように実装する
// 同様に、クライアント証明書がある場合は userInteraction で読み取れるようにしておく
clientSession.setPasswordIdentityProvider(PasswordIdentityProvider.wrapPasswords(pass_word));
log("authorization");
clientSession.auth().verify();
// //xterm などを自力で実装する場合
// PtyChannelConfiguration pty = new PtyChannelConfiguration();
// pty.setPtyColumns(SCREEN_COLUMNS);
// pty.setPtyLines(SCREEN_LINES);
// pty.setPtyWidth(SCREEN_WIDTH);
// pty.setPtyHeight(SCREEN_HEIGHT);
// pty.setPtyType("xterm");
//
// // キーボードからの入力をサーバ側に送付する(=PipedOutputStream ps)
// clientPtyChannel = clientSession.createShellChannel(pty, null);
// ps = new PipedOutputStream();
// clientPtyChannel.setIn(new PipedInputStream(ps));
// ps.write(TEXT_FROM_KEYBOARD.getBytes(Charset.forName("utf-8")));
// 〜
clientChannel = clientSession.createExecChannel("echo 'hello world'", null, null);
// コマンド実行なら以下のように waitFor で出力を待って終了でば良いが、
// ループさせながら出力をひろう例を後述する
//ByteArrayOutputStream outBaos = new ByteArrayOutputStream();
//clientChannel.setRedirectErrorStream(true);
//clientChannel.setOut(outBaos);
//log("open");
//clientChannel.open().verify();
//clientChannel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
//log(outBaos.toString());
PipedInputStream pso = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream(pso);
log("set I/O stream");
clientChannel.setRedirectErrorStream(true);
clientChannel.setOut(out);
clientChannel.open().verify();
byte[] tmp = new byte[1024];
boolean is_on = true;
while (is_on) {
Collection<ClientChannelEvent> waitMask = clientChannel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
while (pso.available() > 0) {
int i = pso.read(tmp, 0, 1024);
if (i < 0)
break;
log(new String(tmp, 0, i));
}
if (waitMask.contains(ClientChannelEvent.CLOSED)) {
log("Got channel close: " + waitMask);
is_on = false;
} else if (waitMask.contains(ClientChannelEvent.EOF)) {
log("Got channel EOF: " + waitMask);
is_on = false;
}
if (clientChannel.isClosed()) {
if (pso.available() > 0)
continue;
log("exit-status: " + clientChannel.getExitStatus());
break;
}
try {
Thread.sleep(100);
} catch (Exception e) {
// do nothing
}
}
clientChannel.close();
clientSession.disconnect(0, "");
}
catch (Exception e){
log("Exception:" + e + ", cause:" + e.getCause());
e.printStackTrace();
}
}
// Apache-Mina が提供している ssh 接続・認証用のクラス
// implements して自環境に合わせた処理を実装する
private class MyUserInteraction implements UserInteraction {
@Override
public String resolveAuthPasswordAttempt(ClientSession session) throws Exception {
// 認証発生時に呼ばれるので、
// ユーザからパスワードを要求する処理をここのタイミングで入れる
log("resolveAuthPasswordAttempt. user:" + session.getUsername());
return pass_word;
}
@Override
public KeyPair resolveAuthPublicKeyIdentityAttempt(ClientSession session) throws Exception {
// クライアント証明書認証の場合、
// サーバ側に置いた公開鍵に対応する、クライアントの秘密鍵をここで返す
log("resolveAuthPublicKeyIdentityAttempt. user:" + session.getUsername());
return getKeyPair(cl_private_key, pass_phrase);
//return null; // null だと認証失敗あつかい、セッション終了
}
// 鍵を取り出す処理の例として
private KeyPair getKeyPair(String private_key, String pass_phrase) {
// 秘密鍵のString文字列から認証に必要なKeyPairを得る
try {
FilePasswordProvider filePasswordProvider =
(pass_phrase == null ?
FilePasswordProvider.EMPTY : FilePasswordProvider.of(pass_phrase));
KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser();
for (KeyPair keyPair : loader.loadKeyPairs(null, null, filePasswordProvider, private_key)) {
//log("load next key:" + private_key);
return keyPair;
}
} catch (Exception e) {
// パスワードが違う場合
log("load fail:" + e.getCause());
} catch (NoClassDefFoundError e) { // パスワード EMPTY にしたら必要だった場合
log("load fail:" + e.getCause());
}
return null;
}
@Override
public String[] interactive(ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
// UserAuthKeyboardInteractive で使われるっぽい?
log("name:" + name + ", instruction:" + instruction + ", lang:" + lang );
return new String[0];
}
@Override
public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
log("prompt:" + prompt + ", lang:" + lang);
return "";
}
}
}
上記の MySsh02クラスを初期化時に Context を渡して、 run() で実行して、getLog() で結果を取得します(あるいは logcat で確認)。
android {
packagingOptions {
//> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction
// > 4 files found with path 'META-INF/DEPENDENCIES' from inputs:
// どうやらメタ情報が、
// org.apache.mina-core/sshd-sftp/sshd-core/sshd-common で重複するらしい
resources.excludes.add("META-INF/DEPENDENCIES")
// 同じく'org.bouncycastle:bcpkix-jdk18on:1.79' で重複
resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF")
}
}
dependencies {
implementation 'org.apache.mina:mina-core:2.2.4' // Quick Start Guide より
implementation 'org.apache.sshd:sshd-core:2.15.0'
implementation 'net.i2p.crypto:eddsa:0.3.0' // for ed25519
// implementation 'org.apache.sshd:sshd-sftp:2.15.0' // for SFTP support
// implementation 'org.slf4j:slf4j-api:1.7.36' // 必要に応じて指定?
implementation 'org.bouncycastle:bcprov-jdk18on:1.79' // bouncycastle の上書き
implementation 'org.bouncycastle:bcpkix-jdk18on:1.79' // bouncycastle の上書き
}
JSch 1.55 から Apache-MINA への移行について†基本的にクライアント証明書はそのまま移行できます。
|
|