AndroidJava/sshクライアントをApacheMINAで

Apache-MINA を Android で動かす(2026-04-18)


ssh でサーバに接続して echo 'hello world' を実行する例です。

Apache-MINA は現状 API26(Android8) 以降でしか動かせない? っぽいです。
動かすにあたっての設定のポイントがいくつかあります。

※JSch で実装する方法はこちら -> sshクライアントをJSchで

以下、MySsh02.java

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 で確認)。


対話式でパスワードを入力させたりする場合は、
UserInteraction クラスの各処理が認証発生時に呼ばれるので、
ここでループやらsleepやらで処理を止めつつ、AleartDialogなどで入力をうながす。

処理の流れが分からなかったら、ログ出力を入れまくって logcat から追うのが手っ取り早いと思います。

このサンプルではコマンドの実行結果をループ内で read していますが、
これがxtermなどで shell にログインして対話式で入出力し続けるような場合だと、
サーバからの出力を取得&表示し続けつつ、並行して別スレッドでキーボードからの入力を送り続けるような実装になります。

Androidで使うためには、ソース内のコメントで書いたとおり、
まず最初に仮のHomeディレクトリ設定や、BaouncyCastle の上書きみたいなのが必要になります。
あと、API26未満の古いAndroid端末では動かせない。


build.gradle でもAndroid用にいくつかの記述が必要です。
それぞれバージョンは最新のものに読み替えてください。

特にエラーが出る場合は packageingOptions の部分を参考にしてみてください。

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 の上書き
 
}


暗号関連の処理で BouncyCastle を使っているのですが、
前述のソースコード内のコメントに書いたように、Android側で使っているBCと重複しないようにする記述が必要になります。

暗号化関連でなにか不可解なことが起きたら、BouncyCastle が Android プリセットのものと自分で入れたものの二種類があることを思い出してみてください。


JSch 1.55 から Apache-MINA への移行について

基本的にクライアント証明書はそのまま移行できます。
(JSch 1.55 で作成した証明書を、Apache-MINA でも使うことはできる)
逆に、新しい版から古い版への移行、つまり最新のApache-MINA から古い JSch への移行はできない場合があります。実際、試してみて失敗しました。
(Apache-MINA で作成した証明書が、JSch 1.55 で使えない場合がある。新から旧への互換性なら無くても当然か)

サーバ側の公開鍵・フィンガープリントは移行時に変わってしまいます。
これは単に、JSch 1.55 で未対応の暗号化方式に Apache-MINA では対応しているので、最新の暗号化方式でssh接続しようとした結果(同じサーバでも接続方式が変わるので)フィンガープリントが変わってしまうのです。

Apache MINA というより、BouncyCastle の勉強みたいなところがありますね。特に証明書や暗号関連が。
stackoverflow とかから実装例を探すのですが、暗号化の話とかもう、異世界言語みたいでクラクラしました。




余談として、当時なかなかサンプルや How to を見つけられなかったApacheMINAで実装した方法としては、とにかくAPI仕様をぜんぶ見る、でした。
(AndroidStudioだとメソッドをクリックすれば、そのクラスのファイルにジャンプするので、そこにある各メソッドのAPI仕様をざっと見てみる)

たとえば KeyPair を作成する方法なら、戻り値が KeyPair になっているメソッドをAPI仕様から片っ端から探す。力技のようで、実際それだけでも対象のメソッドはだいぶ限定されるので。
なんかそれっぽいキーワードが見つかったらWeb検索して、その使用例とか仕様とかを探すとか。

KayPair(鍵のペア)って名前から先入観がありましたが、別にペアでなくても、秘密鍵や公開鍵の片方だけが必要な場合でも使うのはKeyPairだったりします。


あと、Web検索するたびに頼んでもないAI回答(しかも消せない)が、本当っぽいウソを並び立ててくるのが、本当に、マジで・・・おかげで、検索エンジンをDuckDuckGoと使い分ける習慣が身につきました。




長くなりましたが、以上です。 このサンプルコードが、実装してみる際のヒントになれば幸いです。
自分でつくったアプリが動くのは、楽しいですよね。