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

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



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

JSch 本家のほうは 1.55 で更新が止まっていて、その後継は mwiede:jsch になります。
ここでは 1.55 で動くサンプルをメモします。

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


以下、MySsh01.java

import android.content.Context;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.PrintWriter;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;

public class MySsh01 {
    private static final String user = "your_user";
    private static final String pass_word = "your_password";
    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 final JSch jsch = new JSch();
    private Session session;
    private Channel channel;
    private boolean isRun;
    MyUserInfo myUserInfo = new MyUserInfo();

    private Context context;
    MySsh01(Context context){
        this.context = context;
    }

    // とりあえずログは全部 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);
            jsch.removeAllIdentity(); // 前回分を破棄

            session = jsch.getSession(user, host, port);

            // 接続処理を対話式にしたい場合は UserInfo を使う
            session.setUserInfo(myUserInfo);
            // userInfo を使わなくても可 
            //session.setPassword(pass_word);

            // クライアント証明書を使う場合
            // 直接String形式で JSch に渡せない(?)ので一時ファイルを経由する
            log("make temporary file");
            File tmp_pem;
            {
                tmp_pem = File.createTempFile("temp.pem", null, context.getCacheDir());
                FileWriter fw2 = new FileWriter(tmp_pem, false);
                BufferedWriter bw2 = new BufferedWriter(fw2);
                PrintWriter pw2 = new PrintWriter(bw2);
                log("cl_key:" + cl_private_key);
                pw2.write(cl_private_key);
                pw2.flush();
                pw2.close();
            }
            jsch.addIdentity(tmp_pem.getPath());
            // tmp_pem.delete();

            // サーバ側の公開鍵を検証する場合は
            // 取得済みの鍵を PUB_KEY_OF_SERVER として
            //session.setConfig("StrictHostKeyChecking", "ask");
            //jsch.getHostKeyRepository().add(new HostKey(host, PUB_KEY_OF_SERVER), null);
            //log("sever_key:" + session.getHostKey().getKey()); // 必要に応じて取得して保存する

            log("connect");
            session.setTimeout(0);
            session.connect();

            log("exec");
            channel = session.openChannel("exec");
            ((ChannelExec) channel).setCommand("echo 'hello world'");

            log("connect");

            // xterm などを自力で実装する場合
            //ChannelShell ch_shell;
            //ch_shell = (ChannelShell) session.openChannel("shell");
            //ch_shell.setPtyType("xterm");
            //ch_shell.setPtySize(SCREEN_COLUMNS, SCREEN_LINES, SCREEN_WIDTH, SCREEN_HEIGHT); // 実際の画面サイズに合わせる
            //InputStream out_shell = new PipedInputStream();
            //PipeOutputStream ps = new PipedOutputStream((PipedInputStream) out_shell);
            //ch_shell.setInputStream(out_shell);
            //ps.write(TEXT_FROM_KEYBOARD.getBytes(Charset.forName("utf-8")));
            // 〜

            InputStream in = channel.getInputStream();
            channel.connect();

            log("connected");

            byte[] tmp = new byte[1024];
            while (true) {
                while (in.available() > 0) {
                    int i = in.read(tmp, 0, 1024);
                    if (i < 0)
                        break;
                    log(new String(tmp, 0, i));
                }
                if (channel.isClosed()) {
                    if (in.available() > 0)
                        continue;
                    log("exit-status: " + channel.getExitStatus());
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    // do nothing
                }
            }

            channel.disconnect();
            session.disconnect();
        }
        catch (Exception e){
            log("Exception:" + e + ", cause:" + e.getCause());
            e.printStackTrace();
        }
    }

    // JSch が提供している ssh 接続・認証用のクラス
    // implements して自環境に合わせた処理を実装する
    private class MyUserInfo implements UserInfo {
        @Override
        public boolean promptPassword(String message){
            // パスワード未設定で接続した時に
            // 「Password for ユーザ名@ホスト名」 とかが message に渡されて呼ばれるので
            // ユーザからパスワードを要求する処理をここのタイミングで入れる
            log("promptPassword:" + message);
            return true; // キャンセルなら false
        }
        @Override
        public String getPassword() {
            // 認証発生時に呼ばれるので、
            // promptPassword などで取得したパスワードを returnする
            // パスワード不一致の場合は promptPasswordが呼ばれる
            //log("getPassword" + pass_word);
            return pass_word;
        }
        @Override
        public boolean promptPassphrase(String message){
            log("promptPassphrase:" + message);
            // クライアント証明書のパスフレーズを要求する処理
            // 内容は promptPassword と同じ
            return true; // キャンセルなら false
        }
        @Override
        public String getPassphrase(){
            // ここで返した証明書パスフレーズが不一致なら promptPassphraseが呼ばれる
            log("getPassphrase:" + pass_phrase);
            return pass_phrase;
        }
        @Override
        public boolean promptYesNo(String message){
            // これが呼ばれた理由は message に入るので、
            // それをユーザに表示しつつ
            // Yes(true) or No(false) を求めて、return で返す。
            // 例として、StrictHostKeyChecking 設定で、鍵が不一致だった場合に
            // 処理を続行するか中断するかを選ばせる、など
            log("promptYesNo:" + message);
            return true; // 拒否なら falseで
        }
        @Override
        public void showMessage(String message){
            // 接続時の「接続しました」的なメッセージなど
            log("showMessage:" + message);
        }

        //public String[] promptKeyboardInteractive(String destination,
        //                                          String name,
        //                                          String instruction,
        //                                          String[] prompt,
        //                                          boolean[] echo){
        //    return null;
        //}
    }
}

上記の MySsh01クラスを初期化時に Context を渡して、 run() で実行して、getLog() で結果を取得します(あるいは logcat で確認)。

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

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


build.gradle には以下の記述を追加。
mwiede:jsch の方は、最新のバージョンに読み替えてください。

dependencies {
 
   implementation 'com.jcraft:jsch:0.1.55'
   // 手元のAndroid端末で、アプリのサイズが 5.8 MBくらい
 
   //implementation 'com.github.mwiede:jsch:2.28.0'
   // 手元のAndroi端末で、アプリのサイズが 10.1 MBくらい
 
}

jsch 1.55 だとED25519をはじめとした後発の暗号化方式には未対応です。
1.55 では読み込めないクライアント証明書が、後継の mwiede:jsch なら読み込めるというケースもあります。

というか、特別な事情がなければ最新の mwiede:jsch を使ったほうが安全なはずです。
mwiede:jsch で実装するなら、最新のAPI仕様書を探せばもっと便利な実装方法やらメソッドやらが見つかると思います。
ものすごく古い端末&環境で動かすとかの事情があるなら、1.55 を使うのもありだと思いますが。

私は mwiede:jsch は使ってないので分かりませんが、
Apache MINA との比較で言えば、ほぼ同じ内容のコードで10.5MBくらい、たいしてサイズに違いはないと思います。

どちらを使うかは好みで選べば良いと思いますが、
特に証明書まわりの処理や sftp の実装方法はかなり異なる印象を受けました。
気になる方は、自分が実装したい機能についての情報がどこにどれだけ落ちているか、先にざっと探してみて感触を確かめてみると良いかもしれません。

ちなみに私が最初に sshクライアントアプリを作った時は JSch 1.55 を使っていました。
当時は Android 4.4 でも動作して、アプリのインストールサイズは 2MB(!?)でした。

最初は特に UserInfo まわりの処理の流れを理解するのにすごく苦戦しました。
おなじくよくわかんない人は、ログ出力を入れまくって実際に動かしてみるのが手っ取り早いと思います。

以上です。いろいろ試してみてください。