├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── README_ZH.md ├── build.rs ├── build_check.sh ├── changelog ├── examples ├── Cargo.toml ├── bio │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── customized_algorithms │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── exec │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── exec_backend │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── scp │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── scp_backend │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── shell │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── shell_backend │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── shell_interactive │ ├── Cargo.toml │ └── src │ │ └── main.rs └── timeout │ ├── Cargo.toml │ └── src │ └── main.rs ├── src ├── algorithm │ ├── compression │ │ ├── mod.rs │ │ └── zlib.rs │ ├── encryption │ │ ├── aes_cbc.rs │ │ ├── aes_ctr.rs │ │ ├── chacha20_poly1305_openssh.rs │ │ ├── des_cbc.rs │ │ └── mod.rs │ ├── hash │ │ ├── hash.rs │ │ ├── hash_ctx.rs │ │ ├── hash_type.rs │ │ └── mod.rs │ ├── key_exchange │ │ ├── curve25519.rs │ │ ├── dh.rs │ │ ├── ecdh_sha2_nistp256.rs │ │ └── mod.rs │ ├── mac │ │ ├── hmac_sha1.rs │ │ ├── hmac_sha2.rs │ │ └── mod.rs │ ├── mod.rs │ └── public_key │ │ ├── dss.rs │ │ ├── ed25519.rs │ │ ├── mod.rs │ │ └── rsa.rs ├── channel │ ├── backend │ │ ├── channel.rs │ │ ├── channel_exec.rs │ │ ├── channel_scp.rs │ │ ├── channel_shell.rs │ │ └── mod.rs │ ├── local │ │ ├── channel.rs │ │ ├── channel_exec.rs │ │ ├── channel_scp.rs │ │ ├── channel_shell.rs │ │ └── mod.rs │ └── mod.rs ├── client │ ├── client.rs │ ├── client_auth.rs │ ├── client_kex.rs │ └── mod.rs ├── config │ ├── algorithm.rs │ ├── auth.rs │ ├── mod.rs │ └── version.rs ├── constant.rs ├── error.rs ├── lib.rs ├── model │ ├── backend_msg.rs │ ├── data.rs │ ├── flow_control.rs │ ├── mod.rs │ ├── packet.rs │ ├── scp_file.rs │ ├── sequence.rs │ ├── terminal.rs │ ├── timeout.rs │ └── u32iter.rs ├── session │ ├── mod.rs │ ├── session_broker.rs │ └── session_local.rs └── util.rs ├── tests ├── algorithms.rs ├── connect.rs ├── exec.rs └── scp.rs └── version /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build 3 | 4 | on: 5 | push: 6 | branches: [ "*" ] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | cargo_fmt: 15 | name: Check cargo formatting 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Run cargo fmt 20 | run: cargo fmt --all -- --check 21 | 22 | cargo_clippy: 23 | name: Check cargo clippy 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Install Clippy 28 | run: rustup component add clippy 29 | - name: Clippy (no features enabled) 30 | run: cargo clippy -- -D warnings 31 | - name: Clippy (all features enabled) 32 | run: cargo clippy --all-features -- -D warnings 33 | 34 | build-linux: 35 | name: Build check on linux 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - name: Build Linux (no features enabled) 40 | run: cargo build --verbose 41 | - name: Build Linux (all features enabled) 42 | run: cargo build --verbose --all-features 43 | 44 | build-wasm32: 45 | name: Build check for wasm32 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v3 49 | - name: Add wasm32 50 | run: rustup target add wasm32-unknown-unknown 51 | - name: Build wasm32 (no features enabled) 52 | run: cargo build --target wasm32-unknown-unknown --verbose 53 | - name: Build wasm32 (all features enabled) 54 | run: cargo build --target wasm32-unknown-unknown --verbose --all-features 55 | 56 | build-windows: 57 | name: Build check on windows 58 | runs-on: windows-2019 59 | steps: 60 | - uses: actions/checkout@v3 61 | - name: Build Windows (no features enabled) 62 | run: cargo build --verbose 63 | - name: Build Windows (all features enabled) 64 | run: cargo build --verbose --all-features 65 | 66 | cargo-test: 67 | name: Check Cargo test 68 | runs-on: ubuntu-latest 69 | container: 70 | image: alpine:latest 71 | env: 72 | SSH_RS_TEST_SERVER: localhost:8888 73 | SSH_RS_TEST_USER: ubuntu 74 | SSH_RS_TEST_PASSWD: password 75 | SSH_RS_TEST_PEM_RSA: /root/rsa_old 76 | SSH_RS_TEST_OPENSSH_RSA: /root/rsa_new 77 | SSH_RS_TEST_ED25519: /root/ed25519 78 | steps: 79 | - uses: actions/checkout@v3 80 | - name: set timezone 81 | run: echo 'Europe/London' > /etc/timezone 82 | - name: install ssh 83 | run: apk add --no-cache --update sudo openssh bash openssh-keygen gcc musl-dev rust cargo 84 | - name: add user 85 | run: addgroup ubuntu && adduser --shell /bin/ash --disabled-password --home /home/ubuntu --ingroup ubuntu ubuntu && echo "ubuntu:password" | chpasswd 86 | - name: config ssh keys 87 | run: ssh-keygen -A 88 | - name: generate dsa keys 89 | run: ssh-keygen -t dsa -b 1024 -N '' -f /etc/ssh/ssh_host_dsa_key 90 | - name: add pubkey authentication 91 | run: sed -i -E "s|(AuthorizedKeysFile).*|\1 %h/.ssh/authorized_keys|g" /etc/ssh/sshd_config 92 | - name: enable password authentication 93 | run: sed -i -E "s/#?(ChallengeResponseAuthentication|PasswordAuthentication).*/\1 yes/g" /etc/ssh/sshd_config 94 | - name: add deprecated pubkeys 95 | run: echo "HostKeyAlgorithms=+ssh-rsa,ssh-dss" >> /etc/ssh/sshd_config && echo "PubkeyAcceptedAlgorithms=+ssh-rsa,ssh-dss" >> /etc/ssh/sshd_config 96 | - name: add deprecated kexes 97 | run: echo "KexAlgorithms=+diffie-hellman-group14-sha1,diffie-hellman-group1-sha1" >> /etc/ssh/sshd_config 98 | - name: add deprecated ciphers 99 | run: echo "Ciphers=+aes128-cbc,3des-cbc,aes192-cbc,aes256-cbc" >> /etc/ssh/sshd_config 100 | - name: add deprecated dsa keys 101 | run: echo "HostKey /etc/ssh/ssh_host_dsa_key" >> /etc/ssh/sshd_config 102 | - name: add rsa keys 103 | run: echo "HostKey /etc/ssh/ssh_host_rsa_key" >> /etc/ssh/sshd_config 104 | - name: add ed25519 keys 105 | run: echo "HostKey /etc/ssh/ssh_host_ed25519_key" >> /etc/ssh/sshd_config 106 | - name: add ecdsa keys 107 | run: echo "HostKey /etc/ssh/ssh_host_ecdsa_key" >> /etc/ssh/sshd_config 108 | - name: create .ssh 109 | run: mkdir -p /home/ubuntu/.ssh && umask 066; touch /home/ubuntu/.ssh/authorized_keys 110 | - name: generate rsa files 111 | run: ssh-keygen -t rsa -b 4096 -m pem -N '' -f /root/rsa_old && cat /root/rsa_old.pub >> /home/ubuntu/.ssh/authorized_keys 112 | - name: generate openssh-rsa files 113 | run: ssh-keygen -t rsa -b 4096 -N '' -f /root/rsa_new && cat /root/rsa_new.pub >> /home/ubuntu/.ssh/authorized_keys 114 | - name: generate ed25519 files 115 | run: ssh-keygen -t ed25519 -N '' -f /root/ed25519 && cat /root/ed25519.pub >> /home/ubuntu/.ssh/authorized_keys 116 | - name: change owner 117 | run: chown -R ubuntu /home/ubuntu/.ssh 118 | - name: run ssh 119 | run: mkdir /run/sshd && /usr/sbin/sshd -T &&/usr/sbin/sshd -D -p 8888 & 120 | - name: Test 121 | run: cargo test --all-features -- --test-threads 1 122 | - name: Doc test 123 | run: cargo test --doc --all-features -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | .gitignore 4 | .vscode 5 | # Generated by Cargo 6 | # will have compiled files and executables 7 | /target/ 8 | 9 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 10 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 11 | Cargo.lock 12 | 13 | # These are backup files generated by rustfmt 14 | **/*.rs.bk 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ssh-rs" 3 | version = "0.5.0" 4 | edition = "2021" 5 | authors = [ 6 | "Gao Xiang Kang <1148118271@qq.com>", 7 | "Jovi Hsu " 8 | ] 9 | description = "In addition to encryption library, pure RUST implementation of SSH-2.0 client protocol" 10 | keywords = ["ssh", "sshAgreement", "sshClient"] 11 | readme = "README.md" 12 | license = "MIT" 13 | repository = "https://github.com/1148118271/ssh-rs" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | [features] 17 | deprecated-algorithms = [ 18 | "deprecated-rsa-sha1", 19 | "deprecated-dh-group1-sha1", 20 | "deprecated-aes-cbc", 21 | "deprecated-des-cbc", 22 | "deprecated-dss-sha1" 23 | ] 24 | deprecated-rsa-sha1 = ["dep:sha1"] 25 | deprecated-dss-sha1 = ["dep:sha1", "dep:dsa"] 26 | deprecated-dh-group1-sha1 = ["dep:sha1"] 27 | deprecated-aes-cbc = ["dep:cbc", "dep:cipher"] 28 | deprecated-des-cbc = ["dep:cbc", "dep:cipher", "dep:des"] 29 | deprecated-zlib = [] 30 | scp = ["dep:filetime"] 31 | 32 | [lib] 33 | name = "ssh" 34 | path = "src/lib.rs" 35 | 36 | [dependencies] 37 | ## error 38 | thiserror = "^1.0" 39 | 40 | ## log 41 | tracing = { version = "0.1.36", features = ["log"] } 42 | 43 | ## string enum 44 | strum = "0.25" 45 | strum_macros = "0.25" 46 | 47 | ## algorithm 48 | rand = "0.8" 49 | num-bigint = { version = "0.4", features = ["rand"] } 50 | # the crate rsa has removed the internal hash implement from 0.7.0 51 | sha1 = { version = "0.10.5", default-features = false, features = ["oid"], optional = true } 52 | sha2 = { version = "0.10.6", default-features = false, features = ["oid"]} 53 | dsa = { version = "0.6.1", optional = true } 54 | rsa = "0.9" 55 | aes = "0.8" 56 | ctr = "0.9" 57 | des = { version = "0.8", optional = true } 58 | cbc = { version = "0.1", optional = true } 59 | cipher = { version = "0.4", optional = true } 60 | ssh-key = { version = "0.6", features = ["rsa", "ed25519", "alloc"]} 61 | signature = "2.1" 62 | ring = "0.17" 63 | 64 | ## compression 65 | flate2 = "^1.0" 66 | 67 | ## utils 68 | filetime = { version = "0.2", optional = true } 69 | 70 | [target.'cfg(target_arch = "wasm32")'.dependencies] 71 | ring = { version = "0.17", features = ["wasm32_unknown_unknown_js"] } 72 | 73 | 74 | [dev-dependencies] 75 | tracing-subscriber = { version = "^0.3" } 76 | paste = "1" 77 | 78 | 79 | [profile.dev] 80 | opt-level = 0 81 | 82 | [profile.release] 83 | opt-level = 3 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Kang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh-rs ✨ 2 | 3 | [![Build](https://github.com/1148118271/ssh-rs/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/1148118271/ssh-rs/actions/workflows/build.yml) 4 | [![API Docs](https://docs.rs/ssh-rs/badge.svg)](https://docs.rs/ssh-rs/latest/) 5 | [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 6 | 7 | [English](https://github.com/1148118271/ssh-rs/blob/main/README.md) | [简体中文](https://github.com/1148118271/ssh-rs/blob/main/README_ZH.md) 8 | 9 | Rust implementation of ssh2.0 client. 10 | 11 | If you encounter any problems in use, welcome [issues](https://github.com/1148118271/ssh-rs/issues) 12 | or [PR](https://github.com/1148118271/ssh-rs/pulls) . 13 | 14 | ## Content 15 | 16 | 17 | 18 | 19 | 20 | * [ssh-rs ✨](#ssh-rs) 21 | + [Content](#content) 22 | + [Connection method:](#connection-method) 23 | - [1. Password:](#1-password) 24 | - [2. Public key:](#2-public-key) 25 | - [1. Use key file path:](#1-use-key-file-path) 26 | - [2. Use key string:](#2-use-key-string) 27 | - [3. Use them together](#3-use-them-together) 28 | + [Enable global logging:](#enable-global-logging) 29 | + [Set timeout:](#set-timeout) 30 | + [How to use:](#how-to-use) 31 | + [Algorithm support:](#algorithm-support) 32 | - [1. Kex algorithms](#1-kex-algorithms) 33 | - [2. Server host key algorithms](#2-server-host-key-algorithms) 34 | - [3. Encryption algorithms (client to server)](#3-encryption-algorithms-client-to-server) 35 | - [4. Encryption algorithms (server to client)](#4-encryption-algorithms-server-to-client) 36 | - [5. Mac algorithms (client to server)](#5-mac-algorithms-client-to-server) 37 | - [6. Mac algorithms (server to client)](#6-mac-algorithms-server-to-client) 38 | - [7. Compression algorithms (client to server)](#7-compression-algorithms-client-to-server) 39 | - [8. Compression algorithms (server to client)](#8-compression-algorithms-server-to-client) 40 | - [☃️ Additional algorithms will continue to be added.](#️-additional-algorithms-will-continue-to-be-added) 41 | 42 | 43 | 44 | ## Connection method: 45 | 46 | ### 1. Password: 47 | 48 | ```rust 49 | use ssh; 50 | 51 | let mut session = ssh::create_session() 52 | .username("ubuntu") 53 | .password("password") 54 | .connect("127.0.0.1:22") 55 | .unwrap(); 56 | ``` 57 | 58 | ### 2. Public key: 59 | 60 | * **Currently, only RSA, ED25519 keys/key files are supported.** 61 | 62 | #### 1. Use key file path: 63 | 64 | ```rust 65 | // pem format key path -> /xxx/xxx/id_rsa 66 | // the content of the keyfile shall begin with 67 | // -----BEGIN RSA PRIVATE KEY----- / -----BEGIN OPENSSH PRIVATE KEY----- 68 | // and end with 69 | // -----END RSA PRIVATE KEY----- / -----END OPENSSH PRIVATE KEY----- 70 | // simply generated by `ssh-keygen -t rsa -m PEM -b 4096` 71 | use ssh; 72 | 73 | let mut session = ssh::create_session() 74 | .username("ubuntu") 75 | .private_key_path("./id_rsa") 76 | .connect("127.0.0.1:22") 77 | .unwrap(); 78 | ``` 79 | 80 | #### 2. Use key string: 81 | 82 | ```rust 83 | // pem format key string: 84 | // -----BEGIN RSA PRIVATE KEY----- / -----BEGIN OPENSSH PRIVATE KEY----- 85 | // and end with 86 | // -----END RSA PRIVATE KEY----- / -----END OPENSSH PRIVATE KEY----- 87 | use ssh; 88 | 89 | let mut session = ssh::create_session() 90 | .username("ubuntu") 91 | .private_key("rsa_string") 92 | .connect("127.0.0.1:22") 93 | .unwrap(); 94 | ``` 95 | 96 | #### 3. Use them together 97 | 98 | * According to the implementation of OpenSSH, it will try public key first and fallback to password. So both of them can be provided. 99 | 100 | ```Rust 101 | use ssh; 102 | 103 | let mut session = ssh::create_session() 104 | .username("username") 105 | .password("password") 106 | .private_key_path("/path/to/rsa") 107 | .connect("127.0.0.1:22") 108 | .unwrap(); 109 | ``` 110 | 111 | ## Enable global logging: 112 | 113 | * This crate now uses the `log` compatible `tracing` for logging functionality 114 | 115 | ```rust 116 | use tracing::Level; 117 | use tracing_subscriber::FmtSubscriber; 118 | 119 | // this will generate some basic event logs 120 | // a builder for `FmtSubscriber`. 121 | let subscriber = FmtSubscriber::builder() 122 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 123 | // will be written to stdout. 124 | .with_max_level(Level::INFO) 125 | // completes the builder. 126 | .finish(); 127 | 128 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 129 | ``` 130 | 131 | ## Set timeout: 132 | 133 | * Only global timeouts per r/w are currently supported. 134 | 135 | ```rust 136 | use ssh; 137 | 138 | let _listener = TcpListener::bind("127.0.0.1:7777").unwrap(); 139 | 140 | match ssh::create_session() 141 | .username("ubuntu") 142 | .password("password") 143 | .private_key_path("./id_rsa") 144 | .timeout(Some(std::time::Duration::from_secs(5))) 145 | .connect("127.0.0.1:7777") 146 | { 147 | Err(e) => println!("Got error {}", e), 148 | _ => unreachable!(), 149 | } 150 | ``` 151 | 152 | ## How to use: 153 | 154 | * Examples can be found under [examples](examples) 155 | 156 | 1. [Execute a command](examples/exec/src/main.rs) 157 | 2. [Scp files](examples/scp/src/main.rs) 158 | 3. [Run a shell](examples/shell/src/main.rs) 159 | 4. [Run an interactive shell](examples/shell_interactive/src/main.rs) 160 | 5. [Connect ssh server w/o a tcp stream](examples/bio/src/main.rs) 161 | 6. [Cofigure your own algorithm list](examples/customized_algorithms/src/main.rs) 162 | 163 | ## Algorithm support: 164 | 165 | ### 1. Kex algorithms 166 | 167 | * `curve25519-sha256` 168 | * `ecdh-sha2-nistp256` 169 | * `diffie-hellman-group14-sha256` 170 | * `diffie-hellman-group14-sha1` 171 | * `diffie-hellman-group1-sha1` (behind feature "deprecated-dh-group1-sha1") 172 | 173 | ### 2. Server host key algorithms 174 | 175 | * `ssh-ed25519` 176 | * `rsa-sha2-256` 177 | * `rsa-sha2-512` 178 | * `rsa-sha` (behind feature "deprecated-rsa-sha1") 179 | * `ssh-dss` (behind feature "deprecated-dss-sha1") 180 | 181 | 182 | ### 3. Encryption algorithms 183 | 184 | * `chacha20-poly1305@openssh.com` 185 | * `aes128-ctr` 186 | * `aes192-ctr` 187 | * `aes256-ctr` 188 | * `aes128-cbc` (behind feature "deprecated-aes-cbc") 189 | * `aes192-cbc` (behind feature "deprecated-aes-cbc") 190 | * `aes256-cbc` (behind feature "deprecated-aes-cbc") 191 | * `3des-cbc` (behind feature "deprecated-des-cbc") 192 | 193 | ### 4. Mac algorithms 194 | 195 | * `hmac-sha2-256` 196 | * `hmac-sha2-512` 197 | * `hmac-sha1` 198 | 199 | ### 5. Compression algorithms 200 | 201 | * `none` 202 | * `zlib@openssh.com` 203 | * `zlib` (behind feature "zlib") 204 | 205 | --- 206 | 207 | ### ☃️ Additional algorithms will continue to be added. 208 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # ssh-rs ✨ 2 | 3 | [English](https://github.com/1148118271/ssh-rs/blob/main/README.md) | [简体中文](https://github.com/1148118271/ssh-rs/blob/main/README_ZH.md) 4 | 5 | rust实现的ssh2.0客户端。 6 | 7 | 如果在使用中遇到任何问题,欢迎 [issues](https://github.com/1148118271/ssh-rs/issues) 8 | 或者 [PR](https://github.com/1148118271/ssh-rs/pulls) 。 9 | 10 | ### 连接方式: 11 | 12 | #### 1. 密码连接: 13 | 14 | ```rust 15 | fn main() { 16 | let session = ssh::create_session() 17 | .username("ubuntu") 18 | .password("password") 19 | .connect("ip:port") 20 | .unwrap() 21 | .run_local(); 22 | } 23 | ``` 24 | 25 | #### 2. 公钥连接: 26 | 27 | ```rust 28 | fn main() { 29 | let session = ssh::create_session() 30 | .username("ubuntu") 31 | .password("password") 32 | .private_key_path("./id_rsa") // 文件地址 33 | .connect("ip:port") 34 | .unwrap() 35 | .run_local(); 36 | } 37 | ``` 38 | 39 | ```rust 40 | fn main() { 41 | let session = ssh::create_session() 42 | .username("ubuntu") 43 | .password("password") 44 | .private_key("rsa_string") // 文件字符串 45 | .connect("ip:port") 46 | .unwrap() 47 | .run_local(); 48 | } 49 | ``` 50 | 51 | ### 启用全局日志: 52 | 本crate现在使用兼容`log`的`tracing` crate记录log 53 | 使用下面的代码片段启用log 54 | ```rust 55 | use tracing::Level; 56 | use tracing_subscriber::FmtSubscriber; 57 | // this will generate some basic event logs 58 | // a builder for `FmtSubscriber`. 59 | let subscriber = FmtSubscriber::builder() 60 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 61 | // will be written to stdout. 62 | .with_max_level(Level::INFO) 63 | // completes the builder. 64 | .finish(); 65 | 66 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 67 | ``` 68 | 69 | ### 设置全局超时时间: 70 | 71 | ```rust 72 | ssh::create_session().timeout(Some(std::time::Duration::from_secs(5))); 73 | ``` 74 | 75 | ### 使用样例 76 | * 更多使用样例请参考[examples](examples)目录 77 | 78 | 1. [执行单个命令](examples/exec/src/main.rs) 79 | 2. [通过scp传输文件](examples/scp/src/main.rs) 80 | 3. [启动一个pty](examples/shell/src/main.rs) 81 | 4. [运行一个交互式的shell](examples/shell_interactive/src/main.rs) 82 | 5. [使用非tcp连接](examples/bio/src/main.rs) 83 | 6. [自行配置密码组](examples/customized_algorithms/src/main.rs) 84 | 85 | 86 | ### 算法支持: 87 | 88 | #### 1. 密钥交换算法 89 | 90 | * `curve25519-sha256` 91 | * `ecdh-sha2-nistp256` 92 | 93 | #### 2. 主机密钥算法 94 | 95 | * `ssh-ed25519` 96 | * `rsa-sha2-512` 97 | * `rsa-sha2-256` 98 | * `rsa-sha` (features = ["deprecated-rsa-sha1"]) 99 | * `ssh-dss` (features = ["deprecated-dss-sha1"]) 100 | 101 | #### 3. 加密算法 102 | 103 | * `chacha20-poly1305@openssh.com` 104 | * `aes128-ctr` 105 | * `aes192-ctr` 106 | * `aes256-ctr` 107 | * `aes128-cbc` (features = ["deprecated-aes-cbc"]) 108 | * `aes192-cbc` (features = ["deprecated-aes-cbc"]) 109 | * `aes256-cbc` (features = ["deprecated-aes-cbc"]) 110 | * `3des-cbc` (features = ["deprecated-des-cbc"]) 111 | 112 | #### 4. MAC算法 113 | 114 | * `hmac-sha2-256` 115 | * `hmac-sha2-512` 116 | * `hmac-sha1` 117 | 118 | #### 5. 压缩算法 119 | 120 | * `none` 121 | * `zlib` (behind feature "zlib") 122 | 123 | --- 124 | 125 | #### ☃️ 会继续添加其它算法。 126 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | fn main() { 4 | let current_version = match std::env::var("CARGO_PKG_VERSION") { 5 | Ok(v) => v, 6 | Err(_) => return, 7 | }; 8 | let last_version = match fs::read_to_string("version") { 9 | Ok(v) => v.trim().to_string(), 10 | Err(_) => return, 11 | }; 12 | if current_version.eq(&last_version) { 13 | return; 14 | } 15 | 16 | println!("cargo:warning= current version: {current_version} last version: {last_version}"); 17 | 18 | // lib.rs 19 | if replace_lib(¤t_version, &last_version).is_err() { 20 | return; 21 | } 22 | // constant.rs 23 | if replace_constant(¤t_version, &last_version).is_err() { 24 | return; 25 | } 26 | // Cargo.toml 27 | let _ = replace_cargo(¤t_version, &last_version); 28 | } 29 | 30 | fn replace_constant(current_version: &str, last_version: &str) -> Result<(), ()> { 31 | let current_str = format!("SSH-2.0-SSH_RS-{current_version}"); 32 | let last_str = format!("SSH-2.0-SSH_RS-{last_version}"); 33 | replace_file("src/constant.rs", current_str, last_str) 34 | } 35 | 36 | fn replace_lib(current_version: &str, last_version: &str) -> Result<(), ()> { 37 | let current_str = format!("ssh-rs = \"{current_version}\""); 38 | let last_str = format!("ssh-rs = \"{last_version}\""); 39 | replace_file("src/lib.rs", current_str, last_str) 40 | } 41 | 42 | fn replace_cargo(current_version: &str, last_version: &str) -> Result<(), ()> { 43 | let current_str = format!("version = \"{current_version}\""); 44 | let last_str = format!("version = \"{last_version}\""); 45 | replace_file("Cargo.toml", current_str, last_str) 46 | } 47 | 48 | fn replace_file(file_path: &str, current_str: String, last_str: String) -> Result<(), ()> { 49 | let buf_str = match fs::read_to_string(file_path) { 50 | Ok(v) => v, 51 | Err(_) => return Err(()), 52 | }; 53 | let new_buf_str = buf_str.replace(¤t_str, &last_str); 54 | if fs::write(file_path, new_buf_str).is_err() { 55 | return Err(()); 56 | } 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /build_check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | echo format check 4 | cargo fmt --all -- --check > /dev/null 5 | echo done 6 | echo 7 | echo 8 | echo clippy check 9 | cargo clippy -- -D warnings > /dev/null 10 | echo 11 | echo 12 | echo clippy all check 13 | cargo clippy --all-features -- -D warnings > /dev/null 14 | echo 15 | echo 16 | echo linux build check 17 | cargo build --target x86_64-unknown-linux-gnu > /dev/null 18 | echo done 19 | echo 20 | echo 21 | echo wasm build check 22 | cargo build --target wasm32-unknown-unknown > /dev/null 23 | echo done 24 | echo 25 | echo 26 | echo windows build check 27 | cargo build --target x86_64-pc-windows-gnu > /dev/null 28 | echo done 29 | echo 30 | echo 31 | echo cargo test 32 | cargo test -- --test-threads 1 > /dev/null 33 | echo done -------------------------------------------------------------------------------- /changelog: -------------------------------------------------------------------------------- 1 | v0.5.0 (2023-12-26) 2 | 1. Fix some time the global timeout will exceed 3 | 2. Add new API to support `TcpStream::connect_timeout` 4 | 3. Add new APIs to support getting command exit status 5 | 4. pub `LocalChannel` & `ChannelBroker` some of their methods are necessary 6 | 7 | v0.4.5 (2023-11-17) 8 | 1. Fix the high cpu usage caused by non_block tcp 9 | 2. Fix the failuer of version agreement if the server sends more than one lines 10 | 11 | v0.4.4 (2023-11-15) 12 | 1. Remove some debug print 13 | 2. Fix the panic when connect to non-ssh servers 14 | 3. Start the ssh-version negotiations as soon as the connection established 15 | 16 | v0.4.3 (2023-10-18) 17 | 1. Bump ring to 0.17 18 | 2. Add ssh-dss support (behind feature deprecated-dss-sha1) 19 | 20 | v0.4.2 (2023-10-13) 21 | 1. Bump trace version, see #75 for more details 22 | 2. Bugfix: Do not panic at non-ssh server connections, see #77 for more 23 | details 24 | 25 | v0.4.1 (2023-09-20) 26 | 1. Add zlib, zlib@openssh.com support 27 | 28 | v0.4.0 (2023-09-16) 29 | 1. remove chinese comments 30 | 2. add RFC links 31 | 3. remove the self-implemented log, using tracing instead 32 | 4. move scp related function behind feature `scp' 33 | 5. re-implement the ssh-error to derive thiserror crate 34 | 6. rename the dangerous-related features to deprecated-* 35 | 7. add aes-128/192/256-cbc encryption modes (behind feature deprecated-aes-cbc) 36 | 7. add 3des-cbc encryption modes (behind feature deprecated-des-cbc) 37 | 38 | v0.3.3 (2023-09-10) 39 | 1. fix hang when tcp connects to a non-existent host 40 | 2. refactor aes_ctr file 41 | 3. translate the changelogs 42 | 4. use std::time::Duration as timeout rather than u128 43 | 5. add the support for ssh message `SSH_MSG_CHANNEL_EXTENDED_DATA` 44 | 6. bump dependencies 45 | 46 | v0.3.2 (2023-01-10) 47 | 1. fix some error with hmac2 48 | 2. add aes-192-crt, aes-256-ctr 49 | 50 | v0.3.1 (2022-12-07) 51 | fix some issues 52 | 53 | v0.3.0 (2022-11-18) 54 | 1. code refactor 55 | 2. disable ssh-rsa by default, move it behind feature "dangerous-algorithms" 56 | 57 | v0.2.2 (2022-11-05) 58 | 1. add connect_bio API which allows connection over any read/write objects. 59 | 2. implement key exchanges during a connected connection 60 | 61 | v0.2.1 (2022-09-26) 62 | 1. fix sometimes unexpected timeout 63 | 64 | v0.2.0 (2022-08-29) 65 | 1. add aes_ctr_128 66 | 2. add hmac_sha1 67 | 3. set tcp non-block by default 68 | 4. add public_key auth 69 | 70 | v0.1.5 (2022-06-13) 71 | 1. modify the accessibility of ChannelScp 72 | 73 | v0.1.4 (2022-05-31) 74 | 1. remove all mutex 75 | 2. fix issues with window size 76 | 3. add scp upload & download 77 | 78 | v0.1.3 (2022-01-17): 79 | 1. code refactor 80 | 2. add log 81 | 3. open channel directly from session 82 | 83 | v0.1.2 (2022-01-9): 84 | 1. fix shell channel cannot be using among threads (Incompatible from ver 0.3) 85 | 2. fix that one session cannot open multiple channels 86 | 3. remove chrono 87 | 88 | v0.1.1 (2022-01-5): 89 | 1. fix crashes 90 | 91 | v0.1.0 (2022-01-5): 92 | 1. implement the basic ssh protocol 93 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "*", 4 | ] 5 | 6 | exclude = [ 7 | "target" 8 | ] -------------------------------------------------------------------------------- /examples/bio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bio" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../" } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } 12 | -------------------------------------------------------------------------------- /examples/bio/src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::net::{TcpStream, ToSocketAddrs}; 3 | use tracing::Level; 4 | use tracing_subscriber::FmtSubscriber; 5 | 6 | fn main() { 7 | // a builder for `FmtSubscriber`. 8 | let subscriber = FmtSubscriber::builder() 9 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 10 | // will be written to stdout. 11 | .with_max_level(Level::INFO) 12 | // completes the builder. 13 | .finish(); 14 | 15 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 16 | 17 | let bio = MyProxy::new("127.0.0.1:22"); 18 | 19 | let mut session = ssh::create_session() 20 | .username("ubuntu") 21 | .password("password") 22 | .private_key_path("./id_rsa") 23 | .connect_bio(bio) 24 | .unwrap() 25 | .run_local(); 26 | let exec = session.open_exec().unwrap(); 27 | let vec: Vec = exec.send_command("ls -all").unwrap(); 28 | println!("{}", String::from_utf8(vec).unwrap()); 29 | // Close session. 30 | session.close(); 31 | } 32 | 33 | // Use a real ssh server since I don't wanna implement a ssh-server in the example codes 34 | struct MyProxy { 35 | server: TcpStream, 36 | } 37 | 38 | impl MyProxy { 39 | fn new(addr: A) -> Self 40 | where 41 | A: ToSocketAddrs, 42 | { 43 | Self { 44 | server: TcpStream::connect(addr).unwrap(), 45 | } 46 | } 47 | } 48 | 49 | impl std::io::Read for MyProxy { 50 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 51 | println!("bio log: read {} bytes", buf.len()); 52 | self.server.read(buf) 53 | } 54 | } 55 | 56 | impl std::io::Write for MyProxy { 57 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 58 | println!("bio log: write {} bytes", buf.len()); 59 | self.server.write(buf) 60 | } 61 | 62 | fn flush(&mut self) -> std::io::Result<()> { 63 | self.server.flush() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/customized_algorithms/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "customized_algorithms" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path="../../", features = ["deprecated-algorithms"]} 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } 12 | -------------------------------------------------------------------------------- /examples/customized_algorithms/src/main.rs: -------------------------------------------------------------------------------- 1 | use ssh::algorithm; 2 | use tracing::Level; 3 | use tracing_subscriber::FmtSubscriber; 4 | 5 | fn main() { 6 | // a builder for `FmtSubscriber`. 7 | let subscriber = FmtSubscriber::builder() 8 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 9 | // will be written to stdout. 10 | .with_max_level(Level::INFO) 11 | // completes the builder. 12 | .finish(); 13 | 14 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 15 | 16 | let mut session = ssh::create_session_without_default() 17 | .username("ubuntu") 18 | .private_key_path("./id_rsa") 19 | .password("password") 20 | .add_kex_algorithms(algorithm::Kex::Curve25519Sha256) 21 | .add_kex_algorithms(algorithm::Kex::EcdhSha2Nistrp256) 22 | .del_kex_algorithms(algorithm::Kex::Curve25519Sha256) 23 | .add_pubkey_algorithms(algorithm::PubKey::SshRsa) 24 | .add_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) 25 | .del_pubkey_algorithms(algorithm::PubKey::RsaSha2_256) 26 | .add_enc_algorithms(algorithm::Enc::Chacha20Poly1305Openssh) 27 | .add_compress_algorithms(algorithm::Compress::None) 28 | .add_mac_algortihms(algorithm::Mac::HmacSha1) 29 | .connect("127.0.0.1:22") 30 | .unwrap() 31 | .run_local(); 32 | 33 | let exec = session.open_exec().unwrap(); 34 | let vec: Vec = exec.send_command("ls -all").unwrap(); 35 | println!("{}", String::from_utf8(vec).unwrap()); 36 | // Close session. 37 | session.close(); 38 | } 39 | -------------------------------------------------------------------------------- /examples/exec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exec" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../" } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } -------------------------------------------------------------------------------- /examples/exec/src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | use tracing::Level; 3 | use tracing_subscriber::FmtSubscriber; 4 | 5 | fn main() { 6 | // a builder for `FmtSubscriber`. 7 | let subscriber = FmtSubscriber::builder() 8 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 9 | // will be written to stdout. 10 | .with_max_level(Level::INFO) 11 | // completes the builder. 12 | .finish(); 13 | 14 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 15 | 16 | let mut session = ssh::create_session() 17 | .username("ubuntu") 18 | .password("password") 19 | .private_key_path("./id_rsa") 20 | .connect("127.0.0.1:22") 21 | .unwrap() 22 | .run_local(); 23 | let exec = session.open_exec().unwrap(); 24 | let vec: Vec = exec.send_command("ls -all").unwrap(); 25 | println!("{}", String::from_utf8(vec).unwrap()); 26 | 27 | let mut exec = session.open_exec().unwrap(); 28 | exec.exec_command("no_command").unwrap(); 29 | let vec = exec.get_output().unwrap(); 30 | println!("output: {}", String::from_utf8(vec).unwrap()); 31 | println!("exit status: {}", exec.exit_status().unwrap()); 32 | println!("terminated msg: {}", exec.terminate_msg().unwrap()); 33 | let _ = exec.close(); 34 | 35 | // Close session. 36 | session.close(); 37 | } 38 | -------------------------------------------------------------------------------- /examples/exec_backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exec_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../" } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } -------------------------------------------------------------------------------- /examples/exec_backend/src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | use tracing::Level; 3 | use tracing_subscriber::FmtSubscriber; 4 | 5 | fn main() { 6 | // a builder for `FmtSubscriber`. 7 | let subscriber = FmtSubscriber::builder() 8 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 9 | // will be written to stdout. 10 | .with_max_level(Level::INFO) 11 | // completes the builder. 12 | .finish(); 13 | 14 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 15 | 16 | let mut session = ssh::create_session() 17 | .username("ubuntu") 18 | .password("password") 19 | .private_key_path("./id_rsa") 20 | .connect("127.0.0.1:22") 21 | .unwrap() 22 | .run_backend(); 23 | let mut exec = session.open_exec().unwrap(); 24 | 25 | const CMD: &str = "no command"; 26 | 27 | // send the command to server 28 | println!("Send command {}", CMD); 29 | exec.send_command(CMD).unwrap(); 30 | 31 | // process other data 32 | println!("Do someother thing"); 33 | 34 | // get command result 35 | let vec: Vec = exec.get_result().unwrap(); 36 | println!("output: {}", String::from_utf8(vec).unwrap()); 37 | println!("exit status: {}", exec.exit_status().unwrap()); 38 | println!("terminated msg: {}", exec.terminate_msg().unwrap()); 39 | 40 | // Close session. 41 | session.close(); 42 | } 43 | -------------------------------------------------------------------------------- /examples/scp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scp" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../", features = ["scp"] } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } 12 | -------------------------------------------------------------------------------- /examples/scp/src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | use tracing::Level; 3 | use tracing_subscriber::FmtSubscriber; 4 | 5 | fn main() { 6 | // a builder for `FmtSubscriber`. 7 | let subscriber = FmtSubscriber::builder() 8 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 9 | // will be written to stdout. 10 | .with_max_level(Level::INFO) 11 | // completes the builder. 12 | .finish(); 13 | 14 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 15 | 16 | let mut session = ssh::create_session() 17 | .username("ubuntu") 18 | .password("password") 19 | .private_key_path("./id_rsa") 20 | .connect("127.0.0.1:22") 21 | .unwrap() 22 | .run_local(); 23 | 24 | // upload a file 25 | let scp = session.open_scp().unwrap(); 26 | scp.upload("./src/main.rs", "./").unwrap(); 27 | assert_file("/home/ubuntu/main.rs"); 28 | 29 | // upload with a rename 30 | let scp = session.open_scp().unwrap(); 31 | scp.upload("./src/main.rs", "./abc").unwrap(); 32 | assert_file("/home/ubuntu/abc"); 33 | 34 | // upload to an implicit dir 35 | let scp = session.open_scp().unwrap(); 36 | scp.upload("./src/main.rs", "./test").unwrap(); 37 | assert_file("/home/ubuntu/test/main.rs"); 38 | 39 | // upload a dir 40 | let scp = session.open_scp().unwrap(); 41 | scp.upload("./src", "./test").unwrap(); 42 | assert_file("/home/ubuntu/test/src/main.rs"); 43 | assert_dir("/home/ubuntu/test/src"); 44 | 45 | // upload a with a rename 46 | let scp = session.open_scp().unwrap(); 47 | scp.upload("./src", "./crs").unwrap(); 48 | assert_file("/home/ubuntu/crs/main.rs"); 49 | assert_dir("/home/ubuntu/crs"); 50 | 51 | // download a file 52 | let scp = session.open_scp().unwrap(); 53 | scp.download("./", "test/a").unwrap(); 54 | assert_file("./a"); 55 | 56 | // download with a rename 57 | let scp = session.open_scp().unwrap(); 58 | scp.download("./b", "test/a").unwrap(); 59 | assert_file("./b"); 60 | 61 | // download to an implicit dir 62 | let scp = session.open_scp().unwrap(); 63 | let _ = std::fs::create_dir("dir"); 64 | scp.download("./dir", "test/a").unwrap(); 65 | assert_file("./dir/a"); 66 | assert_dir("./dir"); 67 | 68 | // download a dir 69 | let scp = session.open_scp().unwrap(); 70 | scp.download("./", "test").unwrap(); 71 | assert_file("./test/a"); 72 | assert_dir("./test"); 73 | 74 | // download with a rename 75 | let scp = session.open_scp().unwrap(); 76 | scp.download("./dir2", "test").unwrap(); 77 | assert_file("./dir2/a"); 78 | assert_dir("./dir2"); 79 | 80 | // download with a rename #2 81 | let scp = session.open_scp().unwrap(); 82 | scp.download("./dir2/", "test").unwrap(); 83 | assert_file("./dir2/a"); 84 | assert_dir("./dir2"); 85 | 86 | session.close(); 87 | } 88 | 89 | fn assert_file(filename: &str) { 90 | let file = std::path::Path::new(filename); 91 | 92 | println!("Assert file {}", filename); 93 | assert!(file.exists()); 94 | 95 | std::fs::remove_file(file).unwrap(); 96 | } 97 | 98 | fn assert_dir(dirname: &str) { 99 | let dir = std::path::Path::new(dirname); 100 | 101 | println!("Assert dir {}", dirname); 102 | assert!(dir.exists()); 103 | 104 | std::fs::remove_dir(dir).unwrap(); 105 | } 106 | -------------------------------------------------------------------------------- /examples/scp_backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scp_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../", features = ["scp"] } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } 12 | -------------------------------------------------------------------------------- /examples/scp_backend/src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | use tracing::Level; 3 | use tracing_subscriber::FmtSubscriber; 4 | 5 | fn main() { 6 | // a builder for `FmtSubscriber`. 7 | let subscriber = FmtSubscriber::builder() 8 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 9 | // will be written to stdout. 10 | .with_max_level(Level::INFO) 11 | // completes the builder. 12 | .finish(); 13 | 14 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 15 | let mut session = ssh::create_session() 16 | .username("ubuntu") 17 | .password("password") 18 | .private_key_path("./id_rsa") 19 | .connect("127.0.0.1:22") 20 | .unwrap() 21 | .run_backend(); 22 | 23 | // upload a file 24 | let scp = session.open_scp().unwrap(); 25 | // currently upload cannot automatically run in the backend 26 | scp.upload("./src/main.rs", "./").unwrap(); 27 | assert_file("/home/ubuntu/main.rs"); 28 | 29 | // download a file 30 | let mut scp = session.open_scp().unwrap(); 31 | scp.start_download("./", "test/a").unwrap(); 32 | println!("Doing some other things"); 33 | assert_no_file("./a"); 34 | scp.end_download().unwrap(); 35 | assert_file("./a"); 36 | 37 | session.close(); 38 | } 39 | 40 | fn assert_file(filename: &str) { 41 | let file = std::path::Path::new(filename); 42 | 43 | println!("Assert file {}", filename); 44 | assert!(file.exists()); 45 | 46 | std::fs::remove_file(file).unwrap(); 47 | } 48 | 49 | fn assert_no_file(filename: &str) { 50 | let file = std::path::Path::new(filename); 51 | 52 | println!("Assert no file {}", filename); 53 | assert!(!file.exists()); 54 | } 55 | -------------------------------------------------------------------------------- /examples/shell/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shell" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../" } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } 12 | -------------------------------------------------------------------------------- /examples/shell/src/main.rs: -------------------------------------------------------------------------------- 1 | use ssh::{self, LocalShell, SshError}; 2 | use tracing::Level; 3 | use tracing_subscriber::FmtSubscriber; 4 | 5 | use std::time::Duration; 6 | 7 | fn main() { 8 | // a builder for `FmtSubscriber`. 9 | let subscriber = FmtSubscriber::builder() 10 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 11 | // will be written to stdout. 12 | .with_max_level(Level::INFO) 13 | // completes the builder. 14 | .finish(); 15 | 16 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 17 | 18 | let mut session = ssh::create_session() 19 | .username("ubuntu") 20 | .password("password") 21 | .timeout(Some(Duration::from_millis(1000))) 22 | .private_key_path("./id_rsa") 23 | .connect("127.0.0.1:22") 24 | .unwrap() 25 | .run_local(); 26 | // Usage 1 27 | let mut shell = session.open_shell().unwrap(); 28 | run_shell(&mut shell); 29 | 30 | // Close channel. 31 | shell.close().unwrap(); 32 | // Close session. 33 | session.close(); 34 | } 35 | 36 | fn run_shell(shell: &mut LocalShell) { 37 | let out = shell.read().unwrap(); 38 | print!("{}", String::from_utf8(out).unwrap()); 39 | 40 | shell.write(b"ls -lah\n").unwrap(); 41 | 42 | loop { 43 | match shell.read() { 44 | Ok(out) => print!("{}", String::from_utf8(out).unwrap()), 45 | Err(e) => { 46 | if let SshError::TimeoutError = e { 47 | break; 48 | } else { 49 | panic!("{}", e.to_string()) 50 | } 51 | } 52 | } 53 | } 54 | 55 | let _ = shell.close(); 56 | println!("exit status: {}", shell.exit_status().unwrap()); 57 | println!("terminated msg: {}", shell.terminate_msg().unwrap()); 58 | } 59 | -------------------------------------------------------------------------------- /examples/shell_backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shell_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../" } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } 12 | -------------------------------------------------------------------------------- /examples/shell_backend/src/main.rs: -------------------------------------------------------------------------------- 1 | use ssh::{self, ShellBrocker}; 2 | use std::thread::sleep; 3 | use std::time::Duration; 4 | use tracing::Level; 5 | use tracing_subscriber::FmtSubscriber; 6 | 7 | fn main() { 8 | // a builder for `FmtSubscriber`. 9 | let subscriber = FmtSubscriber::builder() 10 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 11 | // will be written to stdout. 12 | .with_max_level(Level::INFO) 13 | // completes the builder. 14 | .finish(); 15 | 16 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 17 | 18 | let mut session = ssh::create_session() 19 | .username("ubuntu") 20 | .password("password") 21 | .private_key_path("./id_rsa") 22 | .connect("127.0.0.1:22") 23 | .unwrap() 24 | .run_backend(); 25 | // Usage 1 26 | let mut shell = session.open_shell().unwrap(); 27 | run_shell(&mut shell); 28 | 29 | // Close channel. 30 | shell.close().unwrap(); 31 | sleep(Duration::from_secs(2)); 32 | println!("exit status: {}", shell.exit_status().unwrap()); 33 | println!("terminated msg: {}", shell.terminate_msg().unwrap()); 34 | // Close session. 35 | session.close(); 36 | } 37 | 38 | fn run_shell(shell: &mut ShellBrocker) { 39 | let vec = shell.read().unwrap(); 40 | println!("{}", String::from_utf8(vec).unwrap()); 41 | 42 | shell.write(b"ls -all\n").unwrap(); 43 | 44 | sleep(Duration::from_secs(2)); 45 | let vec = shell.read().unwrap(); 46 | println!("{}", String::from_utf8(vec).unwrap()); 47 | } 48 | -------------------------------------------------------------------------------- /examples/shell_interactive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shell_interactive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../" } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } 12 | # nix = "0.25.0" 13 | mio = { version="0.8.5", features = ["os-poll", "net", "os-ext"]} -------------------------------------------------------------------------------- /examples/shell_interactive/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use mio::unix::SourceFd; 3 | 4 | use std::fs::File; 5 | #[cfg(unix)] 6 | use std::os::unix::io::FromRawFd; 7 | use std::time::Duration; 8 | use std::{cell::RefCell, rc::Rc}; 9 | use tracing::Level; 10 | use tracing_subscriber::FmtSubscriber; 11 | 12 | use mio::net::TcpStream; 13 | use mio::{event::Source, Events, Interest, Poll, Token}; 14 | 15 | const SERVER: Token = Token(0); 16 | const SHELL: Token = Token(1); 17 | 18 | #[cfg(not(unix))] 19 | fn main() { 20 | panic!("This example can run on unix only") 21 | } 22 | 23 | #[cfg(unix)] 24 | fn main() { 25 | use std::{io::Read, os::unix::prelude::AsRawFd}; 26 | 27 | // a builder for `FmtSubscriber`. 28 | let subscriber = FmtSubscriber::builder() 29 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 30 | // will be written to stdout. 31 | .with_max_level(Level::INFO) 32 | // completes the builder. 33 | .finish(); 34 | 35 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 36 | let tcp = TcpStream::connect("127.0.0.1:22".parse().unwrap()).unwrap(); 37 | let mut session = ssh::create_session() 38 | .username("ubuntu") 39 | .password("password") 40 | .timeout(Some(Duration::from_millis(1000))) 41 | .private_key_path("./id_rsa") 42 | .connect_bio(tcp) 43 | .unwrap() 44 | .run_local(); 45 | 46 | let mut shell = session.open_shell().unwrap(); 47 | let mut tcp_wrap = TcpWrap::new(session.get_raw_io()); 48 | let mut std_in = unsafe { File::from_raw_fd(0) }; 49 | 50 | let mut poll = Poll::new().unwrap(); 51 | let mut events = Events::with_capacity(1024); 52 | poll.registry() 53 | .register(&mut tcp_wrap, SERVER, Interest::READABLE) 54 | .unwrap(); 55 | poll.registry() 56 | .register( 57 | &mut SourceFd(&std_in.as_raw_fd()), 58 | SHELL, 59 | Interest::READABLE, 60 | ) 61 | .unwrap(); 62 | 63 | let mut buf = [0; 2048]; 64 | 65 | 'main_loop: loop { 66 | poll.poll(&mut events, None).unwrap(); 67 | 68 | for event in &events { 69 | match event.token() { 70 | SERVER => match shell.read() { 71 | Ok(buf) => print!("{}", String::from_utf8_lossy(&buf)), 72 | _ => break 'main_loop, 73 | }, 74 | SHELL => { 75 | let len = std_in.read(&mut buf).unwrap(); 76 | shell.write(&buf[..len]).unwrap(); 77 | } 78 | _ => break 'main_loop, 79 | } 80 | } 81 | } 82 | session.close(); 83 | } 84 | struct TcpWrap { 85 | server: Rc>, 86 | } 87 | 88 | impl TcpWrap { 89 | fn new(tcp: Rc>) -> Self { 90 | Self { server: tcp } 91 | } 92 | } 93 | 94 | impl std::io::Read for TcpWrap { 95 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 96 | println!("bio log: read {} bytes", buf.len()); 97 | self.server.borrow_mut().read(buf) 98 | } 99 | } 100 | 101 | impl std::io::Write for TcpWrap { 102 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 103 | println!("bio log: write {} bytes", buf.len()); 104 | self.server.borrow_mut().write(buf) 105 | } 106 | 107 | fn flush(&mut self) -> std::io::Result<()> { 108 | self.server.borrow_mut().flush() 109 | } 110 | } 111 | 112 | impl Source for TcpWrap { 113 | fn deregister(&mut self, registry: &mio::Registry) -> std::io::Result<()> { 114 | self.server.borrow_mut().deregister(registry) 115 | } 116 | fn register( 117 | &mut self, 118 | registry: &mio::Registry, 119 | token: Token, 120 | interests: Interest, 121 | ) -> std::io::Result<()> { 122 | self.server 123 | .borrow_mut() 124 | .register(registry, token, interests) 125 | } 126 | 127 | fn reregister( 128 | &mut self, 129 | registry: &mio::Registry, 130 | token: Token, 131 | interests: Interest, 132 | ) -> std::io::Result<()> { 133 | self.server 134 | .borrow_mut() 135 | .reregister(registry, token, interests) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /examples/timeout/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "timeout" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ssh-rs = { path = "../../" } 10 | tracing = { version = "^0.1", features = ["log"] } 11 | tracing-subscriber = { version = "^0.3" } -------------------------------------------------------------------------------- /examples/timeout/src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::net::TcpListener; 3 | use std::time::Duration; 4 | use tracing::Level; 5 | use tracing_subscriber::FmtSubscriber; 6 | 7 | fn main() { 8 | // a builder for `FmtSubscriber`. 9 | let subscriber = FmtSubscriber::builder() 10 | // all spans/events with a level higher than INFO (e.g, info, warn, etc.) 11 | // will be written to stdout. 12 | .with_max_level(Level::INFO) 13 | // completes the builder. 14 | .finish(); 15 | 16 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 17 | 18 | let _listener = TcpListener::bind("127.0.0.1:7777").unwrap(); 19 | match ssh::create_session() 20 | .username("ubuntu") 21 | .password("password") 22 | .private_key_path("./id_rsa") 23 | .timeout(Some(Duration::from_secs(5))) 24 | .connect_with_timeout("127.0.0.1:7777", Some(Duration::from_secs(2))) 25 | { 26 | Err(e) => println!("Got error {}", e), 27 | _ => unreachable!(), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/algorithm/compression/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Compress; 2 | use crate::SshResult; 3 | 4 | mod zlib; 5 | /// 6 | pub(crate) trait Compression: Send + Sync { 7 | fn new() -> Self 8 | where 9 | Self: Sized; 10 | // The "zlib@openssh.com" method operates identically to the "zlib" 11 | // method described in [RFC4252] except that packet compression does not 12 | // start until the server sends a SSH_MSG_USERAUTH_SUCCESS packet 13 | // so 14 | // fn start(); 15 | fn compress(&mut self, buf: &[u8]) -> SshResult>; 16 | fn decompress(&mut self, buf: &[u8]) -> SshResult>; 17 | } 18 | 19 | pub(crate) fn from(comp: &Compress) -> Box { 20 | match comp { 21 | Compress::None => Box::new(CompressNone::new()), 22 | #[cfg(feature = "deprecated-zlib")] 23 | Compress::Zlib => Box::new(zlib::CompressZlib::new()), 24 | Compress::ZlibOpenSsh => Box::new(zlib::CompressZlib::new()), 25 | } 26 | } 27 | 28 | #[derive(Default)] 29 | pub(crate) struct CompressNone {} 30 | 31 | impl Compression for CompressNone { 32 | fn new() -> Self { 33 | Self {} 34 | } 35 | 36 | fn compress(&mut self, buf: &[u8]) -> SshResult> { 37 | Ok(buf.to_vec()) 38 | } 39 | 40 | fn decompress(&mut self, buf: &[u8]) -> SshResult> { 41 | Ok(buf.to_vec()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/algorithm/compression/zlib.rs: -------------------------------------------------------------------------------- 1 | use flate2; 2 | 3 | use crate::SshError; 4 | 5 | use super::Compression; 6 | 7 | /// The "zlib" compression is described in [RFC1950] and in [RFC1951]. 8 | /// The compression context is initialized after each key exchange, and 9 | /// is passed from one packet to the next, with only a partial flush 10 | /// being performed at the end of each packet. A partial flush means 11 | /// that the current compressed block is ended and all data will be 12 | /// output. If the current block is not a stored block, one or more 13 | /// empty blocks are added after the current block to ensure that there 14 | /// are at least 8 bits, counting from the start of the end-of-block code 15 | /// of the current block to the end of the packet payload. 16 | /// 17 | /// 18 | /// The "zlib@openssh.com" method operates identically to the "zlib" 19 | /// method described in [RFC4252] except that packet compression does not 20 | /// start until the server sends a SSH_MSG_USERAUTH_SUCCESS packet, 21 | /// replacing the "zlib" method's start of compression when the server 22 | /// sends SSH_MSG_NEWKEYS. 23 | pub(super) struct CompressZlib { 24 | decompressor: flate2::Decompress, 25 | compressor: flate2::Compress, 26 | } 27 | 28 | impl Compression for CompressZlib { 29 | fn new() -> Self 30 | where 31 | Self: Sized, 32 | { 33 | Self { 34 | decompressor: flate2::Decompress::new(true), 35 | compressor: flate2::Compress::new(flate2::Compression::fast(), true), 36 | } 37 | } 38 | 39 | fn decompress(&mut self, buf: &[u8]) -> crate::SshResult> { 40 | let mut buf_in = buf; 41 | let mut buf_once = [0; 4096]; 42 | let mut buf_out = vec![]; 43 | loop { 44 | let in_before = self.decompressor.total_in(); 45 | let out_before = self.decompressor.total_out(); 46 | 47 | let result = 48 | self.decompressor 49 | .decompress(buf_in, &mut buf_once, flate2::FlushDecompress::Sync); 50 | 51 | let consumed = (self.decompressor.total_in() - in_before) as usize; 52 | let produced = (self.decompressor.total_out() - out_before) as usize; 53 | 54 | match result { 55 | Ok(flate2::Status::Ok) => { 56 | buf_in = &buf_in[consumed..]; 57 | buf_out.extend(&buf_once[..produced]); 58 | } 59 | Ok(flate2::Status::StreamEnd) => { 60 | return Err(SshError::CompressionError( 61 | "Stream ends during the decompress".to_owned(), 62 | )); 63 | } 64 | Ok(flate2::Status::BufError) => { 65 | break; 66 | } 67 | Err(e) => return Err(SshError::CompressionError(e.to_string())), 68 | } 69 | } 70 | 71 | Ok(buf_out) 72 | } 73 | 74 | fn compress(&mut self, buf: &[u8]) -> crate::SshResult> { 75 | let mut buf_in = buf; 76 | let mut buf_once = [0; 4096]; 77 | let mut buf_out = vec![]; 78 | loop { 79 | let in_before = self.compressor.total_in(); 80 | let out_before = self.compressor.total_out(); 81 | 82 | let result = 83 | self.compressor 84 | .compress(buf_in, &mut buf_once, flate2::FlushCompress::Partial); 85 | 86 | let consumed = (self.compressor.total_in() - in_before) as usize; 87 | let produced = (self.compressor.total_out() - out_before) as usize; 88 | 89 | // tracing::info!(consumed); 90 | // tracing::info!(produced); 91 | 92 | // means an empty compress 93 | // 2 bytes ZLIB header at the start of the stream 94 | // 4 bytes CRC checksum at the end of the stream 95 | if produced == 6 { 96 | break; 97 | } 98 | 99 | match result { 100 | Ok(flate2::Status::Ok) => { 101 | buf_in = &buf_in[consumed..]; 102 | buf_out.extend(&buf_once[..produced]); 103 | } 104 | Ok(flate2::Status::StreamEnd) => { 105 | return Err(SshError::CompressionError( 106 | "Stream ends during the compress".to_owned(), 107 | )); 108 | } 109 | Ok(flate2::Status::BufError) => { 110 | break; 111 | } 112 | Err(e) => return Err(SshError::CompressionError(e.to_string())), 113 | } 114 | } 115 | 116 | Ok(buf_out) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/algorithm/encryption/aes_cbc.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | algorithm::{hash::Hash, mac::Mac}, 3 | SshError, SshResult, 4 | }; 5 | use aes::{ 6 | cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}, 7 | Aes128, Aes192, Aes256, 8 | }; 9 | use cipher::generic_array::GenericArray; 10 | 11 | use super::Encryption; 12 | 13 | const CBC128_KEY_SIZE: usize = 16; 14 | const CBC192_KEY_SIZE: usize = 24; 15 | const CBC256_KEY_SIZE: usize = 32; 16 | const IV_SIZE: usize = 16; 17 | const BLOCK_SIZE: usize = 16; 18 | 19 | struct Extend { 20 | // hmac 21 | mac: Box, 22 | ik_c_s: Vec, 23 | ik_s_c: Vec, 24 | } 25 | 26 | impl Extend { 27 | fn from(mac: Box, ik_c_s: Vec, ik_s_c: Vec) -> Self { 28 | Extend { 29 | mac, 30 | ik_c_s, 31 | ik_s_c, 32 | } 33 | } 34 | } 35 | 36 | macro_rules! crate_aes_cbc { 37 | ($name: ident, $alg: ident, $key_size: expr) => { 38 | pub(super) struct $name { 39 | pub(super) client_key: cbc::Encryptor<$alg>, 40 | pub(super) server_key: cbc::Decryptor<$alg>, 41 | extend: Extend, 42 | } 43 | 44 | impl Encryption for $name { 45 | fn bsize(&self) -> usize { 46 | BLOCK_SIZE 47 | } 48 | 49 | fn iv_size(&self) -> usize { 50 | IV_SIZE 51 | } 52 | 53 | fn new(hash: Hash, mac: Box) -> Self 54 | where 55 | Self: Sized, 56 | { 57 | let (ck, sk) = hash.mix_ek($key_size); 58 | let mut ckey = [0u8; $key_size]; 59 | let mut skey = [0u8; $key_size]; 60 | ckey.clone_from_slice(&ck[..$key_size]); 61 | skey.clone_from_slice(&sk[..$key_size]); 62 | 63 | let mut civ = [0u8; IV_SIZE]; 64 | let mut siv = [0u8; IV_SIZE]; 65 | civ.clone_from_slice(&hash.iv_c_s[..IV_SIZE]); 66 | siv.clone_from_slice(&hash.iv_s_c[..IV_SIZE]); 67 | 68 | let c = cbc::Encryptor::<$alg>::new(&ckey.into(), &civ.into()); 69 | let r = cbc::Decryptor::<$alg>::new(&skey.into(), &siv.into()); 70 | // hmac 71 | let (ik_c_s, ik_s_c) = hash.mix_ik(mac.bsize()); 72 | $name { 73 | client_key: c, 74 | server_key: r, 75 | extend: Extend::from(mac, ik_c_s, ik_s_c), 76 | } 77 | } 78 | 79 | fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { 80 | let len = buf.len(); 81 | let tag = self 82 | .extend 83 | .mac 84 | .sign(&self.extend.ik_c_s, client_sequence_num, buf); 85 | let mut idx = 0; 86 | while idx < len { 87 | let mut block = GenericArray::clone_from_slice(&buf[idx..idx + BLOCK_SIZE]); 88 | self.client_key.encrypt_block_mut(&mut block); 89 | buf[idx..idx + BLOCK_SIZE].clone_from_slice(&block); 90 | 91 | idx += BLOCK_SIZE; 92 | } 93 | buf.extend(tag.as_ref()) 94 | } 95 | 96 | fn decrypt( 97 | &mut self, 98 | server_sequence_number: u32, 99 | buf: &mut [u8], 100 | ) -> SshResult> { 101 | let pl = self.packet_len(server_sequence_number, buf); 102 | let data = &mut buf[..(pl + self.extend.mac.bsize())]; 103 | let (d, m) = data.split_at_mut(pl); 104 | 105 | let len = d.len(); 106 | let mut idx = 0; 107 | while idx < len { 108 | let mut block = GenericArray::clone_from_slice(&d[idx..idx + BLOCK_SIZE]); 109 | self.server_key.decrypt_block_mut(&mut block); 110 | d[idx..idx + BLOCK_SIZE].clone_from_slice(&block); 111 | 112 | idx += BLOCK_SIZE; 113 | } 114 | 115 | let tag = self 116 | .extend 117 | .mac 118 | .sign(&self.extend.ik_s_c, server_sequence_number, d); 119 | let t = tag.as_ref(); 120 | if m != t { 121 | return Err(SshError::EncryptionError( 122 | "Failed to decrypt the server traffic".to_owned(), 123 | )); 124 | } 125 | Ok(d.to_vec()) 126 | } 127 | 128 | fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { 129 | let mut block = GenericArray::clone_from_slice(&buf[..BLOCK_SIZE]); 130 | self.server_key.clone().decrypt_block_mut(&mut block); 131 | let packet_len = u32::from_be_bytes(block[..4].try_into().unwrap()); 132 | (packet_len + 4) as usize 133 | } 134 | 135 | fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { 136 | let pl = self.packet_len(server_sequence_number, buf); 137 | let bsize = self.extend.mac.bsize(); 138 | pl + bsize 139 | } 140 | 141 | fn no_pad(&self) -> bool { 142 | false 143 | } 144 | } 145 | }; 146 | } 147 | 148 | // aes-128-cbc 149 | crate_aes_cbc!(Cbc128, Aes128, CBC128_KEY_SIZE); 150 | // aes-192-cbc 151 | crate_aes_cbc!(Cbc192, Aes192, CBC192_KEY_SIZE); 152 | // aes-256-cbc 153 | crate_aes_cbc!(Cbc256, Aes256, CBC256_KEY_SIZE); 154 | -------------------------------------------------------------------------------- /src/algorithm/encryption/aes_ctr.rs: -------------------------------------------------------------------------------- 1 | use crate::algorithm::encryption::Encryption; 2 | use crate::algorithm::hash::Hash; 3 | use crate::algorithm::mac::Mac; 4 | use crate::error::SshError; 5 | use crate::SshResult; 6 | use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; 7 | use ctr; 8 | 9 | type Aes128Ctr64BE = ctr::Ctr64BE; 10 | type Aes192Ctr64BE = ctr::Ctr64BE; 11 | type Aes256Ctr64BE = ctr::Ctr64BE; 12 | 13 | const CTR128_KEY_SIZE: usize = 16; 14 | const CTR192_KEY_SIZE: usize = 24; 15 | const CTR256_KEY_SIZE: usize = 32; 16 | const IV_SIZE: usize = 16; 17 | const BLOCK_SIZE: usize = 16; 18 | 19 | // extend data for data encryption 20 | struct Extend { 21 | // hmac 22 | mac: Box, 23 | ik_c_s: Vec, 24 | ik_s_c: Vec, 25 | } 26 | impl Extend { 27 | fn from(mac: Box, ik_c_s: Vec, ik_s_c: Vec) -> Self { 28 | Extend { 29 | mac, 30 | ik_c_s, 31 | ik_s_c, 32 | } 33 | } 34 | } 35 | 36 | macro_rules! crate_aes_ctr { 37 | ($name: ident, $alg: ident, $key_size: expr) => { 38 | pub(super) struct $name { 39 | pub(super) client_key: $alg, 40 | pub(super) server_key: $alg, 41 | extend: Extend, 42 | } 43 | 44 | impl Encryption for $name { 45 | fn bsize(&self) -> usize { 46 | BLOCK_SIZE 47 | } 48 | 49 | fn iv_size(&self) -> usize { 50 | IV_SIZE 51 | } 52 | 53 | fn new(hash: Hash, mac: Box) -> Self 54 | where 55 | Self: Sized, 56 | { 57 | let (ck, sk) = hash.mix_ek($key_size); 58 | let mut ckey = [0u8; $key_size]; 59 | let mut skey = [0u8; $key_size]; 60 | ckey.clone_from_slice(&ck[..$key_size]); 61 | skey.clone_from_slice(&sk[..$key_size]); 62 | 63 | let mut civ = [0u8; IV_SIZE]; 64 | let mut siv = [0u8; IV_SIZE]; 65 | civ.clone_from_slice(&hash.iv_c_s[..IV_SIZE]); 66 | siv.clone_from_slice(&hash.iv_s_c[..IV_SIZE]); 67 | 68 | let c = $alg::new(&ckey.into(), &civ.into()); 69 | let r = $alg::new(&skey.into(), &siv.into()); 70 | // hmac 71 | let (ik_c_s, ik_s_c) = hash.mix_ik(mac.bsize()); 72 | $name { 73 | client_key: c, 74 | server_key: r, 75 | extend: Extend::from(mac, ik_c_s, ik_s_c), 76 | } 77 | } 78 | 79 | fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { 80 | let tag = self 81 | .extend 82 | .mac 83 | .sign(&self.extend.ik_c_s, client_sequence_num, buf); 84 | self.client_key.apply_keystream(buf); 85 | buf.extend(tag.as_ref()) 86 | } 87 | 88 | fn decrypt( 89 | &mut self, 90 | server_sequence_number: u32, 91 | buf: &mut [u8], 92 | ) -> SshResult> { 93 | let pl = self.packet_len(server_sequence_number, buf); 94 | let data = &mut buf[..(pl + self.extend.mac.bsize())]; 95 | let (d, m) = data.split_at_mut(pl); 96 | self.server_key.apply_keystream(d); 97 | let tag = self 98 | .extend 99 | .mac 100 | .sign(&self.extend.ik_s_c, server_sequence_number, d); 101 | let t = tag.as_ref(); 102 | if m != t { 103 | return Err(SshError::EncryptionError( 104 | "Failed to decrypt the server traffic".to_owned(), 105 | )); 106 | } 107 | Ok(d.to_vec()) 108 | } 109 | 110 | fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { 111 | let bsize = self.bsize(); 112 | let mut r = vec![0_u8; bsize]; 113 | r.clone_from_slice(&buf[..bsize]); 114 | self.server_key.apply_keystream(&mut r); 115 | let pos: usize = self.server_key.current_pos(); 116 | self.server_key.seek(pos - bsize); 117 | let packet_len = u32::from_be_bytes(r[..4].try_into().unwrap()); 118 | (packet_len + 4) as usize 119 | } 120 | 121 | fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { 122 | let pl = self.packet_len(server_sequence_number, buf); 123 | let bsize = self.extend.mac.bsize(); 124 | pl + bsize 125 | } 126 | 127 | fn no_pad(&self) -> bool { 128 | false 129 | } 130 | } 131 | }; 132 | } 133 | 134 | // aes-128-ctr 135 | crate_aes_ctr!(Ctr128, Aes128Ctr64BE, CTR128_KEY_SIZE); 136 | // aes-192-ctr 137 | crate_aes_ctr!(Ctr192, Aes192Ctr64BE, CTR192_KEY_SIZE); 138 | // aes-256-ctr 139 | crate_aes_ctr!(Ctr256, Aes256Ctr64BE, CTR256_KEY_SIZE); 140 | -------------------------------------------------------------------------------- /src/algorithm/encryption/chacha20_poly1305_openssh.rs: -------------------------------------------------------------------------------- 1 | use crate::algorithm::hash::Hash; 2 | use crate::algorithm::mac::Mac; 3 | use crate::error::SshError; 4 | use crate::{algorithm::encryption::Encryption, error::SshResult}; 5 | use ring::aead::chacha20_poly1305_openssh::{OpeningKey, SealingKey}; 6 | 7 | const KEY_SIZE: usize = 64; 8 | const IV_SIZE: usize = 0; 9 | const BLOCK_SIZE: usize = 0; 10 | const MAC_SIZE: usize = 16; 11 | 12 | pub(super) struct ChaCha20Poly1305 { 13 | client_key: SealingKey, 14 | server_key: OpeningKey, 15 | } 16 | 17 | impl Encryption for ChaCha20Poly1305 { 18 | fn bsize(&self) -> usize { 19 | BLOCK_SIZE 20 | } 21 | 22 | fn iv_size(&self) -> usize { 23 | IV_SIZE 24 | } 25 | 26 | fn new(hash: Hash, _mac: Box) -> ChaCha20Poly1305 { 27 | let (ck, sk) = hash.mix_ek(KEY_SIZE); 28 | let mut sealing_key = [0_u8; KEY_SIZE]; 29 | let mut opening_key = [0_u8; KEY_SIZE]; 30 | sealing_key.copy_from_slice(&ck); 31 | opening_key.copy_from_slice(&sk); 32 | 33 | ChaCha20Poly1305 { 34 | client_key: SealingKey::new(&sealing_key), 35 | server_key: OpeningKey::new(&opening_key), 36 | } 37 | } 38 | 39 | fn encrypt(&mut self, sequence_number: u32, buf: &mut Vec) { 40 | let mut tag = [0_u8; MAC_SIZE]; 41 | self.client_key 42 | .seal_in_place(sequence_number, buf, &mut tag); 43 | buf.append(&mut tag.to_vec()); 44 | } 45 | 46 | fn decrypt(&mut self, sequence_number: u32, buf: &mut [u8]) -> SshResult> { 47 | let mut packet_len_slice = [0_u8; 4]; 48 | let len = &buf[..4]; 49 | packet_len_slice.copy_from_slice(len); 50 | let packet_len_slice = self 51 | .server_key 52 | .decrypt_packet_length(sequence_number, packet_len_slice); 53 | let packet_len = u32::from_be_bytes(packet_len_slice); 54 | let (buf, tag_) = buf.split_at_mut((packet_len + 4) as usize); 55 | let mut tag = [0_u8; MAC_SIZE]; 56 | tag.copy_from_slice(tag_); 57 | match self.server_key.open_in_place(sequence_number, buf, &tag) { 58 | Ok(result) => Ok([&packet_len_slice[..], result].concat()), 59 | Err(_) => Err(SshError::EncryptionError( 60 | "Failed to decrypt the server traffic".to_owned(), 61 | )), 62 | } 63 | } 64 | 65 | fn packet_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize { 66 | let mut packet_len_slice = [0_u8; 4]; 67 | packet_len_slice.copy_from_slice(&buf[..4]); 68 | let packet_len_slice = self 69 | .server_key 70 | .decrypt_packet_length(sequence_number, packet_len_slice); 71 | u32::from_be_bytes(packet_len_slice) as usize + 4 72 | } 73 | 74 | fn data_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize { 75 | let packet_len = self.packet_len(sequence_number, buf); 76 | packet_len + MAC_SIZE 77 | } 78 | 79 | fn no_pad(&self) -> bool { 80 | true 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/algorithm/encryption/des_cbc.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | algorithm::{hash::Hash, mac::Mac}, 3 | SshError, SshResult, 4 | }; 5 | use cipher::{generic_array::GenericArray, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; 6 | use des::TdesEde3 as des; 7 | 8 | use super::Encryption; 9 | 10 | const KEY_SIZE: usize = 24; 11 | const IV_SIZE: usize = 8; 12 | const BLOCK_SIZE: usize = 8; 13 | 14 | struct Extend { 15 | // hmac 16 | mac: Box, 17 | ik_c_s: Vec, 18 | ik_s_c: Vec, 19 | } 20 | 21 | impl Extend { 22 | fn from(mac: Box, ik_c_s: Vec, ik_s_c: Vec) -> Self { 23 | Extend { 24 | mac, 25 | ik_c_s, 26 | ik_s_c, 27 | } 28 | } 29 | } 30 | 31 | pub(super) struct Cbc { 32 | pub(super) client_key: cbc::Encryptor, 33 | pub(super) server_key: cbc::Decryptor, 34 | extend: Extend, 35 | } 36 | 37 | impl Encryption for Cbc { 38 | fn bsize(&self) -> usize { 39 | BLOCK_SIZE 40 | } 41 | 42 | fn iv_size(&self) -> usize { 43 | IV_SIZE 44 | } 45 | 46 | fn new(hash: Hash, mac: Box) -> Self 47 | where 48 | Self: Sized, 49 | { 50 | let (ck, sk) = hash.mix_ek(KEY_SIZE); 51 | let mut ckey = [0u8; KEY_SIZE]; 52 | let mut skey = [0u8; KEY_SIZE]; 53 | ckey.clone_from_slice(&ck[..KEY_SIZE]); 54 | skey.clone_from_slice(&sk[..KEY_SIZE]); 55 | 56 | let mut civ = [0u8; IV_SIZE]; 57 | let mut siv = [0u8; IV_SIZE]; 58 | civ.clone_from_slice(&hash.iv_c_s[..IV_SIZE]); 59 | siv.clone_from_slice(&hash.iv_s_c[..IV_SIZE]); 60 | 61 | let c = cbc::Encryptor::::new(&ckey.into(), &civ.into()); 62 | let r = cbc::Decryptor::::new(&skey.into(), &siv.into()); 63 | // hmac 64 | let (ik_c_s, ik_s_c) = hash.mix_ik(mac.bsize()); 65 | Cbc { 66 | client_key: c, 67 | server_key: r, 68 | extend: Extend::from(mac, ik_c_s, ik_s_c), 69 | } 70 | } 71 | 72 | fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec) { 73 | let len = buf.len(); 74 | let tag = self 75 | .extend 76 | .mac 77 | .sign(&self.extend.ik_c_s, client_sequence_num, buf); 78 | let mut idx = 0; 79 | while idx < len { 80 | let mut block = GenericArray::clone_from_slice(&buf[idx..idx + BLOCK_SIZE]); 81 | self.client_key.encrypt_block_mut(&mut block); 82 | buf[idx..idx + BLOCK_SIZE].clone_from_slice(&block); 83 | 84 | idx += BLOCK_SIZE; 85 | } 86 | buf.extend(tag.as_ref()) 87 | } 88 | 89 | fn decrypt(&mut self, server_sequence_number: u32, buf: &mut [u8]) -> SshResult> { 90 | let pl = self.packet_len(server_sequence_number, buf); 91 | let data = &mut buf[..(pl + self.extend.mac.bsize())]; 92 | let (d, m) = data.split_at_mut(pl); 93 | 94 | let len = d.len(); 95 | let mut idx = 0; 96 | while idx < len { 97 | let mut block = GenericArray::clone_from_slice(&d[idx..idx + BLOCK_SIZE]); 98 | self.server_key.decrypt_block_mut(&mut block); 99 | d[idx..idx + BLOCK_SIZE].clone_from_slice(&block); 100 | 101 | idx += BLOCK_SIZE; 102 | } 103 | 104 | let tag = self 105 | .extend 106 | .mac 107 | .sign(&self.extend.ik_s_c, server_sequence_number, d); 108 | let t = tag.as_ref(); 109 | if m != t { 110 | return Err(SshError::EncryptionError( 111 | "Failed to decrypt the server traffic".to_owned(), 112 | )); 113 | } 114 | Ok(d.to_vec()) 115 | } 116 | 117 | fn packet_len(&mut self, _: u32, buf: &[u8]) -> usize { 118 | let mut block = GenericArray::clone_from_slice(&buf[..BLOCK_SIZE]); 119 | self.server_key.clone().decrypt_block_mut(&mut block); 120 | let packet_len = u32::from_be_bytes(block[..4].try_into().unwrap()); 121 | (packet_len + 4) as usize 122 | } 123 | 124 | fn data_len(&mut self, server_sequence_number: u32, buf: &[u8]) -> usize { 125 | let pl = self.packet_len(server_sequence_number, buf); 126 | let bsize = self.extend.mac.bsize(); 127 | pl + bsize 128 | } 129 | 130 | fn no_pad(&self) -> bool { 131 | false 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/algorithm/encryption/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "deprecated-aes-cbc")] 2 | mod aes_cbc; 3 | mod aes_ctr; 4 | mod chacha20_poly1305_openssh; 5 | #[cfg(feature = "deprecated-des-cbc")] 6 | mod des_cbc; 7 | 8 | use crate::algorithm::hash::Hash; 9 | use crate::algorithm::mac::Mac; 10 | use crate::SshResult; 11 | 12 | use super::{hash::HashCtx, mac::MacNone, Enc}; 13 | 14 | /// 15 | pub(crate) trait Encryption: Send + Sync { 16 | fn bsize(&self) -> usize; 17 | fn iv_size(&self) -> usize; 18 | fn new(hash: Hash, mac: Box) -> Self 19 | where 20 | Self: Sized; 21 | fn encrypt(&mut self, client_sequence_num: u32, buf: &mut Vec); 22 | fn decrypt(&mut self, sequence_number: u32, buf: &mut [u8]) -> SshResult>; 23 | fn packet_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize; 24 | fn data_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize; 25 | fn no_pad(&self) -> bool; 26 | } 27 | 28 | pub(crate) fn from(s: &Enc, hash: Hash, mac: Box) -> Box { 29 | match s { 30 | Enc::Chacha20Poly1305Openssh => { 31 | Box::new(chacha20_poly1305_openssh::ChaCha20Poly1305::new(hash, mac)) 32 | } 33 | Enc::Aes128Ctr => Box::new(aes_ctr::Ctr128::new(hash, mac)), 34 | Enc::Aes192Ctr => Box::new(aes_ctr::Ctr192::new(hash, mac)), 35 | Enc::Aes256Ctr => Box::new(aes_ctr::Ctr256::new(hash, mac)), 36 | #[cfg(feature = "deprecated-aes-cbc")] 37 | Enc::Aes128Cbc => Box::new(aes_cbc::Cbc128::new(hash, mac)), 38 | #[cfg(feature = "deprecated-aes-cbc")] 39 | Enc::Aes192Cbc => Box::new(aes_cbc::Cbc192::new(hash, mac)), 40 | #[cfg(feature = "deprecated-aes-cbc")] 41 | Enc::Aes256Cbc => Box::new(aes_cbc::Cbc256::new(hash, mac)), 42 | #[cfg(feature = "deprecated-des-cbc")] 43 | Enc::TripleDesCbc => Box::new(des_cbc::Cbc::new(hash, mac)), 44 | } 45 | } 46 | 47 | pub(crate) struct EncryptionNone {} 48 | 49 | impl Encryption for EncryptionNone { 50 | fn bsize(&self) -> usize { 51 | 8 52 | } 53 | fn iv_size(&self) -> usize { 54 | 8 55 | } 56 | 57 | fn new(_hash: Hash, _mac: Box) -> Self 58 | where 59 | Self: Sized, 60 | { 61 | Self {} 62 | } 63 | fn encrypt(&mut self, _client_sequence_num: u32, _buf: &mut Vec) { 64 | // do nothing 65 | } 66 | fn decrypt(&mut self, _sequence_number: u32, buf: &mut [u8]) -> SshResult> { 67 | Ok(buf.to_vec()) 68 | } 69 | fn packet_len(&mut self, _sequence_number: u32, buf: &[u8]) -> usize { 70 | u32::from_be_bytes(buf[0..4].try_into().unwrap()) as usize 71 | } 72 | fn data_len(&mut self, sequence_number: u32, buf: &[u8]) -> usize { 73 | self.packet_len(sequence_number, buf) + 4 74 | } 75 | fn no_pad(&self) -> bool { 76 | false 77 | } 78 | } 79 | 80 | impl Default for EncryptionNone { 81 | fn default() -> Self { 82 | let hash = Hash::new(HashCtx::new(), &[], super::hash::HashType::None); 83 | let mac = Box::new(MacNone::new()); 84 | Self::new(hash, mac) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/algorithm/hash/hash.rs: -------------------------------------------------------------------------------- 1 | use super::hash_ctx::HashCtx; 2 | use crate::algorithm::hash; 3 | use crate::algorithm::hash::HashType; 4 | use crate::constant; 5 | 6 | /// 7 | /// 8 | /// The key exchange produces two values: a shared secret K, and an 9 | /// exchange hash H. Encryption and authentication keys are derived from 10 | /// these. The exchange hash H from the first key exchange is 11 | /// additionally used as the session identifier, which is a unique 12 | /// identifier for this connection. It is used by authentication methods 13 | /// as a part of the data that is signed as a proof of possession of a 14 | /// private key. Once computed, the session identifier is not changed, 15 | /// even if keys are later re-exchanged. 16 | 17 | /// Each key exchange method specifies a hash function that is used in 18 | /// the key exchange. The same hash algorithm MUST be used in key 19 | /// derivation. Here, we'll call it HASH. 20 | 21 | /// Encryption keys MUST be computed as HASH, of a known value and K, as 22 | /// follows: 23 | 24 | /// o Initial IV client to server: HASH(K || H || "A" || session_id) 25 | /// (Here K is encoded as mpint and "A" as byte and session_id as raw 26 | /// data. "A" means the single character A, ASCII 65). 27 | 28 | /// o Initial IV server to client: HASH(K || H || "B" || session_id) 29 | 30 | /// o Encryption key client to server: HASH(K || H || "C" || session_id) 31 | 32 | /// o Encryption key server to client: HASH(K || H || "D" || session_id) 33 | 34 | /// o Integrity key client to server: HASH(K || H || "E" || session_id) 35 | 36 | /// o Integrity key server to client: HASH(K || H || "F" || session_id) 37 | 38 | /// Key data MUST be taken from the beginning of the hash output. As 39 | /// many bytes as needed are taken from the beginning of the hash value. 40 | /// If the key length needed is longer than the output of the HASH, the 41 | /// key is extended by computing HASH of the concatenation of K and H and 42 | /// the entire key so far, and appending the resulting bytes (as many as 43 | /// HASH generates) to the key. This process is repeated until enough 44 | /// key material is available; the key is taken from the beginning of 45 | /// this value. In other words: 46 | 47 | /// K1 = HASH(K || H || X || session_id) (X is e.g., "A") 48 | /// K2 = HASH(K || H || K1) 49 | /// K3 = HASH(K || H || K1 || K2) 50 | /// ... 51 | /// key = K1 || K2 || K3 || ... 52 | 53 | /// This process will lose entropy if the amount of entropy in K is 54 | /// larger than the internal state size of HASH. 55 | pub struct Hash { 56 | /// reandom number used once 57 | pub iv_c_s: Vec, 58 | pub iv_s_c: Vec, 59 | 60 | /// key used for data exchange 61 | pub ek_c_s: Vec, 62 | pub ek_s_c: Vec, 63 | 64 | /// key used for hmac 65 | pub ik_c_s: Vec, 66 | pub ik_s_c: Vec, 67 | 68 | hash_type: HashType, 69 | hash_ctx: HashCtx, 70 | } 71 | 72 | impl Hash { 73 | pub fn new(hash_ctx: HashCtx, session_id: &[u8], hash_type: HashType) -> Self { 74 | let k = hash_ctx.k.as_slice(); 75 | let h = hash::digest(&hash_ctx.as_bytes(), hash_type); 76 | let mut keys = vec![]; 77 | for v in constant::ALPHABET { 78 | keys.push(Hash::mix(k, &h, v, session_id, hash_type)); 79 | } 80 | Hash { 81 | iv_c_s: keys[0].clone(), 82 | iv_s_c: keys[1].clone(), 83 | 84 | ek_c_s: keys[2].clone(), 85 | ek_s_c: keys[3].clone(), 86 | 87 | ik_c_s: keys[4].clone(), 88 | ik_s_c: keys[5].clone(), 89 | 90 | hash_type, 91 | hash_ctx, 92 | } 93 | } 94 | 95 | fn mix(k: &[u8], h: &[u8], key_char: u8, session_id: &[u8], hash_type: HashType) -> Vec { 96 | let mut key: Vec = Vec::new(); 97 | key.extend(k); 98 | key.extend(h); 99 | key.push(key_char); 100 | key.extend(session_id); 101 | hash::digest(key.as_slice(), hash_type) 102 | } 103 | 104 | pub fn mix_ek(&self, key_size: usize) -> (Vec, Vec) { 105 | let mut ck = self.ek_c_s.to_vec(); 106 | let mut sk = self.ek_s_c.to_vec(); 107 | while key_size > ck.len() { 108 | ck.extend(self.extend(ck.as_slice())); 109 | sk.extend(self.extend(sk.as_slice())); 110 | } 111 | (ck, sk) 112 | } 113 | 114 | pub fn mix_ik(&self, key_size: usize) -> (Vec, Vec) { 115 | let mut ck = self.ik_c_s.to_vec(); 116 | let mut sk = self.ik_s_c.to_vec(); 117 | while key_size > ck.len() { 118 | ck.extend(self.extend(ck.as_slice())); 119 | sk.extend(self.extend(sk.as_slice())); 120 | } 121 | (ck, sk) 122 | } 123 | 124 | fn extend(&self, key: &[u8]) -> Vec { 125 | let k = self.hash_ctx.k.clone(); 126 | let h = hash::digest(self.hash_ctx.as_bytes().as_slice(), self.hash_type); 127 | let mut hash: Vec = Vec::new(); 128 | hash.extend(k); 129 | hash.extend(h); 130 | hash.extend(key); 131 | hash::digest(hash.as_slice(), self.hash_type) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/algorithm/hash/hash_ctx.rs: -------------------------------------------------------------------------------- 1 | use crate::model::Data; 2 | 3 | /// 4 | /// 5 | /// The key exchange produces two values: a shared secret K, and an 6 | /// exchange hash H. Encryption and authentication keys are derived from 7 | /// these. The exchange hash H from the first key exchange is 8 | /// additionally used as the session identifier, which is a unique 9 | /// identifier for this connection. It is used by authentication methods 10 | /// as a part of the data that is signed as a proof of possession of a 11 | /// private key. Once computed, the session identifier is not changed, 12 | /// even if keys are later re-exchanged. 13 | /// 14 | /// H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K) 15 | /// 16 | /// 17 | #[derive(Clone, Debug, Default)] 18 | pub struct HashCtx { 19 | /// string V_C, the client's identification string (CR and LF excluded) 20 | pub v_c: Vec, 21 | /// string V_S, the server's identification string (CR and LF excluded) 22 | pub v_s: Vec, 23 | 24 | /// string I_C, the payload of the client's SSH_MSG_KEXINIT 25 | pub i_c: Vec, 26 | /// string I_S, the payload of the server's SSH_MSG_KEXINIT 27 | pub i_s: Vec, 28 | 29 | /// string K_S, the host key 30 | pub k_s: Vec, 31 | 32 | /// mpint e, exchange value sent by the client 33 | pub e: Vec, 34 | /// mpint f, exchange value sent by the server 35 | pub f: Vec, 36 | 37 | /// mpint K, the shared secret 38 | pub k: Vec, 39 | } 40 | 41 | impl HashCtx { 42 | pub fn new() -> Self { 43 | HashCtx { 44 | v_c: vec![], 45 | v_s: vec![], 46 | i_c: vec![], 47 | i_s: vec![], 48 | k_s: vec![], 49 | e: vec![], 50 | f: vec![], 51 | k: vec![], 52 | } 53 | } 54 | 55 | pub fn set_v_c(&mut self, vc: &str) { 56 | let mut data = Data::new(); 57 | data.put_str(vc); 58 | self.v_c = data.to_vec(); 59 | } 60 | pub fn set_v_s(&mut self, vs: &str) { 61 | let mut data = Data::new(); 62 | data.put_str(vs); 63 | self.v_s = data.to_vec(); 64 | } 65 | pub fn set_i_c(&mut self, ic: &[u8]) { 66 | let mut data = Data::new(); 67 | data.put_u8s(ic); 68 | self.i_c = data.to_vec(); 69 | } 70 | pub fn set_i_s(&mut self, is: &[u8]) { 71 | let mut data = Data::new(); 72 | data.put_u8s(is); 73 | self.i_s = data.to_vec(); 74 | } 75 | pub fn set_k_s(&mut self, ks: &[u8]) { 76 | let mut data = Data::new(); 77 | data.put_u8s(ks); 78 | self.k_s = data.to_vec(); 79 | } 80 | pub fn set_e(&mut self, qc: &[u8]) { 81 | let mut data = Data::new(); 82 | data.put_u8s(qc); 83 | self.e = data.to_vec(); 84 | } 85 | pub fn set_f(&mut self, qs: &[u8]) { 86 | let mut data = Data::new(); 87 | data.put_u8s(qs); 88 | self.f = data.to_vec(); 89 | } 90 | pub fn set_k(&mut self, k: &[u8]) { 91 | let mut data = Data::new(); 92 | data.put_mpint(k); 93 | self.k = data.to_vec(); 94 | } 95 | 96 | pub fn as_bytes(&self) -> Vec { 97 | let mut v = vec![]; 98 | v.extend(&self.v_c); 99 | v.extend(&self.v_s); 100 | v.extend(&self.i_c); 101 | v.extend(&self.i_s); 102 | v.extend(&self.k_s); 103 | v.extend(&self.e); 104 | v.extend(&self.f); 105 | v.extend(&self.k); 106 | v 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/algorithm/hash/hash_type.rs: -------------------------------------------------------------------------------- 1 | /// The hash type used during kex 2 | /// this is determined by the kex alg 3 | #[derive(Copy, Clone)] 4 | pub enum HashType { 5 | None, 6 | SHA1, 7 | SHA256, 8 | } 9 | -------------------------------------------------------------------------------- /src/algorithm/hash/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::module_inception)] 2 | mod hash; 3 | mod hash_ctx; 4 | mod hash_type; 5 | 6 | pub(crate) use hash::Hash; 7 | pub(crate) use hash_ctx::HashCtx; 8 | pub(crate) use hash_type::HashType; 9 | 10 | pub fn digest(data: &[u8], hash_type: HashType) -> Vec { 11 | let result = match hash_type { 12 | HashType::SHA1 => ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data), 13 | HashType::SHA256 => ring::digest::digest(&ring::digest::SHA256, data), 14 | HashType::None => ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data), // actually doesn't need 15 | }; 16 | result.as_ref().to_vec() 17 | } 18 | -------------------------------------------------------------------------------- /src/algorithm/key_exchange/curve25519.rs: -------------------------------------------------------------------------------- 1 | use super::{super::hash::HashType, KeyExchange}; 2 | use crate::error::SshError; 3 | use crate::SshResult; 4 | use ring::agreement::{EphemeralPrivateKey, PublicKey, UnparsedPublicKey, X25519}; 5 | 6 | pub(super) struct CURVE25519 { 7 | pub private_key: EphemeralPrivateKey, 8 | pub public_key: PublicKey, 9 | } 10 | 11 | impl KeyExchange for CURVE25519 { 12 | fn new() -> SshResult { 13 | let rng = ring::rand::SystemRandom::new(); 14 | let private_key = match EphemeralPrivateKey::generate(&X25519, &rng) { 15 | Ok(v) => v, 16 | Err(e) => return Err(SshError::KexError(e.to_string())), 17 | }; 18 | match private_key.compute_public_key() { 19 | Ok(public_key) => Ok(CURVE25519 { 20 | private_key, 21 | public_key, 22 | }), 23 | Err(e) => Err(SshError::KexError(e.to_string())), 24 | } 25 | } 26 | 27 | fn get_public_key(&self) -> &[u8] { 28 | self.public_key.as_ref() 29 | } 30 | 31 | fn get_shared_secret(&self, puk: Vec) -> SshResult> { 32 | let mut public_key = [0u8; 32]; 33 | public_key.copy_from_slice(&puk); 34 | 35 | let server_pub = UnparsedPublicKey::new(&X25519, public_key); 36 | let private_key = unsafe { (&self.private_key as *const EphemeralPrivateKey).read() }; 37 | crate::algorithm::key_exchange::agree_ephemeral(private_key, &server_pub) 38 | } 39 | 40 | fn get_hash_type(&self) -> HashType { 41 | HashType::SHA256 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/algorithm/key_exchange/dh.rs: -------------------------------------------------------------------------------- 1 | use num_bigint::{BigUint, RandBigInt}; 2 | use std::ops::Shl; 3 | 4 | use crate::SshResult; 5 | 6 | use super::{HashType, KeyExchange}; 7 | 8 | #[cfg(feature = "deprecated-dh-group1-sha1")] 9 | const GROUP1: [u8; 128] = [ 10 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0xf, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 11 | 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x2, 0x4e, 0x8, 0x8a, 0x67, 0xcc, 0x74, 12 | 0x2, 0xb, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x8, 0x79, 0x8e, 0x34, 0x4, 0xdd, 13 | 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0xa, 0x6d, 0xf2, 0x5f, 0x14, 0x37, 14 | 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 15 | 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0xb, 0xff, 0x5c, 0xb6, 0xf4, 0x6, 0xb7, 0xed, 16 | 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, 17 | 0x49, 0x28, 0x66, 0x51, 0xec, 0xe6, 0x53, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 18 | ]; 19 | 20 | const GROUP14: [u8; 256] = [ 21 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0xf, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 22 | 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x2, 0x4e, 0x8, 0x8a, 0x67, 0xcc, 0x74, 23 | 0x2, 0xb, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x8, 0x79, 0x8e, 0x34, 0x4, 0xdd, 24 | 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0xa, 0x6d, 0xf2, 0x5f, 0x14, 0x37, 25 | 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 26 | 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0xb, 0xff, 0x5c, 0xb6, 0xf4, 0x6, 0xb7, 0xed, 27 | 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, 28 | 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x0, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x5, 29 | 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, 30 | 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, 31 | 0x9e, 0xd5, 0x29, 0x7, 0x70, 0x96, 0x96, 0x6d, 0x67, 0xc, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x4, 32 | 0xf1, 0x74, 0x6c, 0x8, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, 33 | 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0xe, 0x86, 0x3, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x7, 0xa2, 0x8f, 34 | 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, 35 | 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x5, 0x10, 36 | 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xac, 0xaa, 0x68, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 37 | ]; 38 | 39 | struct DhGroup { 40 | prime: &'static [u8], 41 | generator: usize, 42 | exp_size: u64, 43 | } 44 | 45 | #[cfg(feature = "deprecated-dh-group1-sha1")] 46 | const DH_GROUP1: DhGroup = DhGroup { 47 | prime: &GROUP1, 48 | generator: 2, 49 | exp_size: 256, 50 | }; 51 | 52 | const DH_GROUP14: DhGroup = DhGroup { 53 | prime: &GROUP14, 54 | generator: 2, 55 | exp_size: 256, 56 | }; 57 | 58 | fn biguint_to_mpint(biguint: &BigUint) -> Vec { 59 | let mut mpint = Vec::new(); 60 | let bytes = biguint.to_bytes_be(); 61 | if let Some(b) = bytes.first() { 62 | if b > &0x7f { 63 | mpint.push(0); 64 | } 65 | } 66 | mpint.extend(&bytes); 67 | mpint 68 | } 69 | 70 | macro_rules! create_dh_with_group { 71 | ($name: ident, $group: expr, $hash: expr) => { 72 | pub(super) struct $name { 73 | prime: BigUint, 74 | private_key: BigUint, 75 | public_key: Vec, 76 | } 77 | 78 | impl KeyExchange for $name { 79 | fn new() -> SshResult { 80 | let mut rng = rand::thread_rng(); 81 | let group = &($group); 82 | let prime = BigUint::from_bytes_be(&group.prime); 83 | let private_key = rng.gen_biguint((group.exp_size * 8) - 2u64).shl(1); 84 | let public_key = 85 | biguint_to_mpint(&BigUint::from(group.generator).modpow(&private_key, &prime)); 86 | 87 | Ok(Self { 88 | prime, 89 | private_key, 90 | public_key, 91 | }) 92 | } 93 | 94 | fn get_public_key(&self) -> &[u8] { 95 | &self.public_key 96 | } 97 | 98 | fn get_shared_secret(&self, puk: Vec) -> SshResult> { 99 | let other_pub = BigUint::from_bytes_be(&puk); 100 | let shared_secret = other_pub.modpow(&self.private_key, &self.prime); 101 | Ok(biguint_to_mpint(&shared_secret)) 102 | } 103 | 104 | fn get_hash_type(&self) -> HashType { 105 | $hash 106 | } 107 | } 108 | }; 109 | } 110 | 111 | #[cfg(feature = "deprecated-dh-group1-sha1")] 112 | create_dh_with_group!(DiffieHellmanGroup1Sha1, DH_GROUP1, HashType::SHA1); 113 | create_dh_with_group!(DiffieHellmanGroup14Sha1, DH_GROUP14, HashType::SHA1); 114 | create_dh_with_group!(DiffieHellmanGroup14Sha256, DH_GROUP14, HashType::SHA256); 115 | -------------------------------------------------------------------------------- /src/algorithm/key_exchange/ecdh_sha2_nistp256.rs: -------------------------------------------------------------------------------- 1 | use super::{super::hash::HashType, KeyExchange}; 2 | use ring::agreement::{EphemeralPrivateKey, PublicKey, UnparsedPublicKey, ECDH_P256}; 3 | 4 | use crate::{SshError, SshResult}; 5 | 6 | pub(super) struct EcdhP256 { 7 | pub private_key: EphemeralPrivateKey, 8 | pub public_key: PublicKey, 9 | } 10 | 11 | impl KeyExchange for EcdhP256 { 12 | fn new() -> SshResult { 13 | let rng = ring::rand::SystemRandom::new(); 14 | let private_key = match EphemeralPrivateKey::generate(&ECDH_P256, &rng) { 15 | Ok(v) => v, 16 | Err(e) => return Err(SshError::KexError(e.to_string())), 17 | }; 18 | match private_key.compute_public_key() { 19 | Ok(public_key) => Ok(EcdhP256 { 20 | private_key, 21 | public_key, 22 | }), 23 | Err(e) => Err(SshError::KexError(e.to_string())), 24 | } 25 | } 26 | 27 | fn get_public_key(&self) -> &[u8] { 28 | self.public_key.as_ref() 29 | } 30 | 31 | fn get_shared_secret(&self, puk: Vec) -> SshResult> { 32 | let mut public_key = [0u8; 65]; 33 | public_key.copy_from_slice(&puk); 34 | let server_pub = UnparsedPublicKey::new(&ECDH_P256, puk); 35 | let private_key = unsafe { (&self.private_key as *const EphemeralPrivateKey).read() }; 36 | crate::algorithm::key_exchange::agree_ephemeral(private_key, &server_pub) 37 | } 38 | 39 | fn get_hash_type(&self) -> HashType { 40 | HashType::SHA256 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/algorithm/key_exchange/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::algorithm::hash::HashType; 2 | use crate::{SshError, SshResult}; 3 | use ring::agreement; 4 | use ring::agreement::{EphemeralPrivateKey, UnparsedPublicKey}; 5 | 6 | /// # Algorithms that used for key exchange 7 | /// 8 | /// 9 | mod curve25519; 10 | mod dh; 11 | mod ecdh_sha2_nistp256; 12 | 13 | use super::Kex; 14 | use curve25519::CURVE25519; 15 | #[cfg(feature = "deprecated-dh-group1-sha1")] 16 | use dh::DiffieHellmanGroup1Sha1; 17 | use dh::{DiffieHellmanGroup14Sha1, DiffieHellmanGroup14Sha256}; 18 | use ecdh_sha2_nistp256::EcdhP256; 19 | 20 | pub(crate) trait KeyExchange: Send + Sync { 21 | fn new() -> SshResult 22 | where 23 | Self: Sized; 24 | fn get_public_key(&self) -> &[u8]; 25 | fn get_shared_secret(&self, puk: Vec) -> SshResult>; 26 | fn get_hash_type(&self) -> HashType; 27 | } 28 | 29 | pub(crate) fn agree_ephemeral>( 30 | private_key: EphemeralPrivateKey, 31 | peer_public_key: &UnparsedPublicKey, 32 | ) -> SshResult> { 33 | match agreement::agree_ephemeral(private_key, peer_public_key, |key_material| { 34 | Ok(key_material.to_vec()) 35 | }) { 36 | Ok(o) => o, 37 | Err(e) => Err(SshError::KexError(e.to_string())), 38 | } 39 | } 40 | 41 | pub(crate) fn from(s: &Kex) -> SshResult> { 42 | match s { 43 | Kex::Curve25519Sha256 => Ok(Box::new(CURVE25519::new()?)), 44 | Kex::EcdhSha2Nistrp256 => Ok(Box::new(EcdhP256::new()?)), 45 | #[cfg(feature = "deprecated-dh-group1-sha1")] 46 | Kex::DiffieHellmanGroup1Sha1 => Ok(Box::new(DiffieHellmanGroup1Sha1::new()?)), 47 | Kex::DiffieHellmanGroup14Sha1 => Ok(Box::new(DiffieHellmanGroup14Sha1::new()?)), 48 | Kex::DiffieHellmanGroup14Sha256 => Ok(Box::new(DiffieHellmanGroup14Sha256::new()?)), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/algorithm/mac/hmac_sha1.rs: -------------------------------------------------------------------------------- 1 | use crate::algorithm::mac::Mac; 2 | use ring::hmac; 3 | use ring::hmac::{Context, Tag}; 4 | 5 | const BSIZE: usize = 20; 6 | 7 | pub(super) struct HMacSha1; 8 | 9 | impl Mac for HMacSha1 { 10 | fn sign(&self, ik: &[u8], sequence_num: u32, buf: &[u8]) -> Tag { 11 | let ik = &ik[..BSIZE]; 12 | let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, ik); 13 | let mut c = Context::with_key(&key); 14 | c.update(sequence_num.to_be_bytes().as_slice()); 15 | c.update(buf); 16 | c.sign() 17 | } 18 | 19 | fn new() -> Self 20 | where 21 | Self: Sized, 22 | { 23 | HMacSha1 24 | } 25 | 26 | fn bsize(&self) -> usize { 27 | BSIZE 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/algorithm/mac/hmac_sha2.rs: -------------------------------------------------------------------------------- 1 | use crate::algorithm::mac::Mac; 2 | use ring::hmac; 3 | use ring::hmac::{Context, Tag}; 4 | 5 | const BSIZE_256: usize = 32; 6 | const BSIZE_512: usize = 64; 7 | 8 | pub(super) struct HmacSha2_256; 9 | pub(super) struct HmacSha2_512; 10 | 11 | impl Mac for HmacSha2_256 { 12 | fn sign(&self, ik: &[u8], sequence_num: u32, buf: &[u8]) -> Tag { 13 | let ik = &ik[..BSIZE_256]; 14 | let key = hmac::Key::new(hmac::HMAC_SHA256, ik); 15 | let mut c = Context::with_key(&key); 16 | c.update(sequence_num.to_be_bytes().as_slice()); 17 | c.update(buf); 18 | c.sign() 19 | } 20 | 21 | fn new() -> Self 22 | where 23 | Self: Sized, 24 | { 25 | HmacSha2_256 26 | } 27 | 28 | fn bsize(&self) -> usize { 29 | BSIZE_256 30 | } 31 | } 32 | 33 | impl Mac for HmacSha2_512 { 34 | fn sign(&self, ik: &[u8], sequence_num: u32, buf: &[u8]) -> Tag { 35 | let ik = &ik[..BSIZE_512]; 36 | let key = hmac::Key::new(hmac::HMAC_SHA512, ik); 37 | let mut c = Context::with_key(&key); 38 | c.update(sequence_num.to_be_bytes().as_slice()); 39 | c.update(buf); 40 | c.sign() 41 | } 42 | 43 | fn new() -> Self 44 | where 45 | Self: Sized, 46 | { 47 | HmacSha2_512 48 | } 49 | 50 | fn bsize(&self) -> usize { 51 | BSIZE_512 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/algorithm/mac/mod.rs: -------------------------------------------------------------------------------- 1 | use ring::hmac::Tag; 2 | 3 | mod hmac_sha1; 4 | mod hmac_sha2; 5 | use hmac_sha1::HMacSha1; 6 | use hmac_sha2::{HmacSha2_256, HmacSha2_512}; 7 | 8 | pub(crate) trait Mac: Send + Sync { 9 | fn sign(&self, ik: &[u8], sequence_num: u32, buf: &[u8]) -> Tag; 10 | fn new() -> Self 11 | where 12 | Self: Sized; 13 | fn bsize(&self) -> usize; 14 | } 15 | 16 | pub(crate) fn from(s: &super::Mac) -> Box { 17 | match s { 18 | super::Mac::HmacSha1 => Box::new(HMacSha1::new()), 19 | super::Mac::HmacSha2_256 => Box::new(HmacSha2_256::new()), 20 | super::Mac::HmacSha2_512 => Box::new(HmacSha2_512::new()), 21 | } 22 | } 23 | 24 | pub(crate) struct MacNone {} 25 | 26 | impl Mac for MacNone { 27 | fn sign(&self, _ik: &[u8], _sequence_num: u32, _buf: &[u8]) -> Tag { 28 | unreachable!() 29 | } 30 | fn new() -> Self 31 | where 32 | Self: Sized, 33 | { 34 | Self {} 35 | } 36 | fn bsize(&self) -> usize { 37 | unreachable!() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/algorithm/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod compression; 2 | pub(crate) mod encryption; 3 | pub(crate) mod hash; 4 | pub(crate) mod key_exchange; 5 | pub(crate) mod mac; 6 | pub(crate) mod public_key; 7 | 8 | use strum_macros::{AsRefStr, EnumString}; 9 | 10 | use self::{hash::HashCtx, key_exchange::KeyExchange}; 11 | 12 | /// symmetrical encryption algorithm 13 | #[derive(Copy, Clone, PartialEq, Eq, AsRefStr, EnumString)] 14 | pub enum Enc { 15 | #[strum(serialize = "chacha20-poly1305@openssh.com")] 16 | Chacha20Poly1305Openssh, 17 | #[strum(serialize = "aes128-ctr")] 18 | Aes128Ctr, 19 | #[strum(serialize = "aes192-ctr")] 20 | Aes192Ctr, 21 | #[strum(serialize = "aes256-ctr")] 22 | Aes256Ctr, 23 | #[cfg(feature = "deprecated-aes-cbc")] 24 | #[strum(serialize = "aes128-cbc")] 25 | Aes128Cbc, 26 | #[cfg(feature = "deprecated-aes-cbc")] 27 | #[strum(serialize = "aes192-cbc")] 28 | Aes192Cbc, 29 | #[cfg(feature = "deprecated-aes-cbc")] 30 | #[strum(serialize = "aes256-cbc")] 31 | Aes256Cbc, 32 | #[cfg(feature = "deprecated-des-cbc")] 33 | #[strum(serialize = "3des-cbc")] 34 | TripleDesCbc, 35 | } 36 | 37 | /// key exchange algorithm 38 | #[derive(Copy, Clone, PartialEq, Eq, AsRefStr, EnumString)] 39 | pub enum Kex { 40 | #[strum(serialize = "curve25519-sha256")] 41 | Curve25519Sha256, 42 | #[strum(serialize = "ecdh-sha2-nistp256")] 43 | EcdhSha2Nistrp256, 44 | #[cfg(feature = "deprecated-dh-group1-sha1")] 45 | #[strum(serialize = "diffie-hellman-group1-sha1")] 46 | DiffieHellmanGroup1Sha1, 47 | #[strum(serialize = "diffie-hellman-group14-sha1")] 48 | DiffieHellmanGroup14Sha1, 49 | #[strum(serialize = "diffie-hellman-group14-sha256")] 50 | DiffieHellmanGroup14Sha256, 51 | } 52 | 53 | /// pubkey hash algorithm 54 | #[derive(Copy, Clone, PartialEq, Eq, AsRefStr, EnumString)] 55 | pub enum PubKey { 56 | #[strum(serialize = "ssh-ed25519")] 57 | SshEd25519, 58 | #[cfg(feature = "deprecated-rsa-sha1")] 59 | #[strum(serialize = "ssh-rsa")] 60 | SshRsa, 61 | #[strum(serialize = "rsa-sha2-256")] 62 | RsaSha2_256, 63 | #[strum(serialize = "rsa-sha2-512")] 64 | RsaSha2_512, 65 | #[cfg(feature = "deprecated-dss-sha1")] 66 | #[strum(serialize = "ssh-dss")] 67 | SshDss, 68 | } 69 | 70 | /// MAC(message authentication code) algorithm 71 | #[derive(Copy, Clone, PartialEq, Eq, AsRefStr, EnumString)] 72 | pub enum Mac { 73 | #[strum(serialize = "hmac-sha1")] 74 | HmacSha1, 75 | #[strum(serialize = "hmac-sha2-256")] 76 | HmacSha2_256, 77 | #[strum(serialize = "hmac-sha2-512")] 78 | HmacSha2_512, 79 | } 80 | 81 | /// compression algorithm 82 | #[derive(Copy, Clone, PartialEq, Eq, AsRefStr, EnumString)] 83 | pub enum Compress { 84 | #[strum(serialize = "none")] 85 | None, 86 | #[cfg(feature = "deprecated-zlib")] 87 | #[strum(serialize = "zlib")] 88 | Zlib, 89 | #[strum(serialize = "zlib@openssh.com")] 90 | ZlibOpenSsh, 91 | } 92 | 93 | #[derive(Default)] 94 | pub(crate) struct Digest { 95 | pub hash_ctx: HashCtx, 96 | pub key_exchange: Option>, 97 | } 98 | 99 | impl Digest { 100 | pub fn new() -> Self { 101 | Self { 102 | ..Default::default() 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/algorithm/public_key/dss.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "deprecated-dss-sha1")] 2 | use sha1::{Digest, Sha1}; 3 | use signature::DigestVerifier; 4 | 5 | use crate::algorithm::public_key::PublicKey as PubK; 6 | use crate::model::Data; 7 | use crate::SshError; 8 | 9 | #[cfg(feature = "deprecated-dss-sha1")] 10 | pub(super) struct DssSha1; 11 | 12 | #[cfg(feature = "deprecated-dss-sha1")] 13 | impl PubK for DssSha1 { 14 | fn new() -> Self 15 | where 16 | Self: Sized, 17 | { 18 | Self 19 | } 20 | 21 | fn verify_signature(&self, ks: &[u8], message: &[u8], sig: &[u8]) -> Result { 22 | let mut data = Data::from(ks[4..].to_vec()); 23 | data.get_u8s(); 24 | 25 | // RFC4253 6.6 DSS Signature key blob are 4x mpint's that need to be pulled out to be used as components in the public key. 26 | let p = dsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 27 | let q = dsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 28 | let g = dsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 29 | let y = dsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 30 | 31 | let components = dsa::Components::from_components(p, q, g).map_err(|_| { 32 | SshError::SshPubKeyError("SSH Public Key components were not valid".to_string()) 33 | })?; 34 | 35 | // Build the public key for verification of the message 36 | let public_key = dsa::VerifyingKey::from_components(components, y).map_err(|_| { 37 | SshError::SshPubKeyError("SSH Public Key components were not valid".to_string()) 38 | })?; 39 | 40 | // Perform an SHA1 hash on the message 41 | let digest = Sha1::new().chain_update(message); 42 | 43 | // RFC4253 6.6 DSS Signature blob is actually 2x160bit blobs so r and s are each 160bit (20 bytes) 44 | let r = dsa::BigUint::from_bytes_be(&sig[0..20]); 45 | let s = dsa::BigUint::from_bytes_be(&sig[20..40]); 46 | 47 | let signature = dsa::Signature::from_components(r, s) 48 | .map_err(|_| SshError::SshPubKeyError("SSH Signature was not valid".to_string()))?; 49 | 50 | // Verify the hashed message with the provided signature, matches the public_key 51 | Ok(public_key.verify_digest(digest, &signature).is_ok()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/algorithm/public_key/ed25519.rs: -------------------------------------------------------------------------------- 1 | use crate::algorithm::public_key::PublicKey; 2 | use crate::model::Data; 3 | use crate::SshError; 4 | use ring::signature; 5 | 6 | pub(super) struct Ed25519; 7 | 8 | impl PublicKey for Ed25519 { 9 | fn new() -> Self 10 | where 11 | Self: Sized, 12 | { 13 | Self 14 | } 15 | 16 | fn verify_signature(&self, ks: &[u8], message: &[u8], sig: &[u8]) -> Result { 17 | let mut data = Data::from(ks[4..].to_vec()); 18 | data.get_u8s(); 19 | let host_key = data.get_u8s(); 20 | let pub_key = signature::UnparsedPublicKey::new(&signature::ED25519, host_key); 21 | Ok(pub_key.verify(message, sig).is_ok()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/algorithm/public_key/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::SshError; 2 | 3 | #[cfg(feature = "deprecated-dss-sha1")] 4 | mod dss; 5 | mod ed25519; 6 | mod rsa; 7 | 8 | #[cfg(feature = "deprecated-dss-sha1")] 9 | use self::dss::DssSha1; 10 | #[cfg(feature = "deprecated-rsa-sha1")] 11 | use self::rsa::RsaSha1; 12 | use self::rsa::RsaSha256; 13 | use self::rsa::RsaSha512; 14 | use super::PubKey; 15 | use ed25519::Ed25519; 16 | 17 | /// # Public Key Algorithms 18 | /// 19 | /// 20 | 21 | pub(crate) trait PublicKey: Send + Sync { 22 | fn new() -> Self 23 | where 24 | Self: Sized; 25 | fn verify_signature(&self, ks: &[u8], message: &[u8], sig: &[u8]) -> Result; 26 | } 27 | 28 | pub(crate) fn from(s: &PubKey) -> Box { 29 | match s { 30 | PubKey::SshEd25519 => Box::new(Ed25519::new()), 31 | #[cfg(feature = "deprecated-rsa-sha1")] 32 | PubKey::SshRsa => Box::new(RsaSha1::new()), 33 | PubKey::RsaSha2_256 => Box::new(RsaSha256::new()), 34 | PubKey::RsaSha2_512 => Box::new(RsaSha512::new()), 35 | #[cfg(feature = "deprecated-dss-sha1")] 36 | PubKey::SshDss => Box::new(DssSha1::new()), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/algorithm/public_key/rsa.rs: -------------------------------------------------------------------------------- 1 | use crate::algorithm::public_key::PublicKey as PubK; 2 | use crate::model::Data; 3 | use crate::SshError; 4 | //use rsa::PublicKey; 5 | use rsa::pkcs1v15::Pkcs1v15Sign; 6 | 7 | pub(super) struct RsaSha256; 8 | 9 | impl PubK for RsaSha256 { 10 | fn new() -> Self 11 | where 12 | Self: Sized, 13 | { 14 | Self 15 | } 16 | 17 | fn verify_signature(&self, ks: &[u8], message: &[u8], sig: &[u8]) -> Result { 18 | let mut data = Data::from(ks[4..].to_vec()); 19 | data.get_u8s(); 20 | 21 | let e = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 22 | let n = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 23 | let public_key = rsa::RsaPublicKey::new(n, e).unwrap(); 24 | let scheme = Pkcs1v15Sign::new::(); 25 | 26 | let digest = ring::digest::digest(&ring::digest::SHA256, message); 27 | let msg = digest.as_ref(); 28 | 29 | Ok(public_key.verify(scheme, msg, sig).is_ok()) 30 | } 31 | } 32 | 33 | pub(super) struct RsaSha512; 34 | 35 | impl PubK for RsaSha512 { 36 | fn new() -> Self 37 | where 38 | Self: Sized, 39 | { 40 | Self 41 | } 42 | 43 | fn verify_signature(&self, ks: &[u8], message: &[u8], sig: &[u8]) -> Result { 44 | let mut data = Data::from(ks[4..].to_vec()); 45 | data.get_u8s(); 46 | 47 | let e = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 48 | let n = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 49 | let public_key = rsa::RsaPublicKey::new(n, e).unwrap(); 50 | let scheme = Pkcs1v15Sign::new::(); 51 | 52 | let digest = ring::digest::digest(&ring::digest::SHA512, message); 53 | let msg = digest.as_ref(); 54 | 55 | Ok(public_key.verify(scheme, msg, sig).is_ok()) 56 | } 57 | } 58 | 59 | #[cfg(feature = "deprecated-rsa-sha1")] 60 | pub(super) struct RsaSha1; 61 | #[cfg(feature = "deprecated-rsa-sha1")] 62 | impl PubK for RsaSha1 { 63 | fn new() -> Self 64 | where 65 | Self: Sized, 66 | { 67 | Self 68 | } 69 | 70 | fn verify_signature(&self, ks: &[u8], message: &[u8], sig: &[u8]) -> Result { 71 | let mut data = Data::from(ks[4..].to_vec()); 72 | data.get_u8s(); 73 | 74 | let e = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 75 | let n = rsa::BigUint::from_bytes_be(data.get_u8s().as_slice()); 76 | let public_key = rsa::RsaPublicKey::new(n, e).unwrap(); 77 | let scheme = Pkcs1v15Sign::new::(); 78 | 79 | let digest = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, message); 80 | let msg = digest.as_ref(); 81 | 82 | Ok(public_key.verify(scheme, msg, sig).is_ok()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/channel/backend/channel_exec.rs: -------------------------------------------------------------------------------- 1 | use super::channel::ChannelBroker; 2 | use crate::error::SshResult; 3 | use crate::model::Data; 4 | use crate::{ 5 | constant::{ssh_connection_code, ssh_str}, 6 | SshError, 7 | }; 8 | use std::ops::{Deref, DerefMut}; 9 | 10 | pub struct ExecBroker { 11 | channel: ChannelBroker, 12 | command_send: bool, 13 | } 14 | 15 | impl ExecBroker { 16 | pub(crate) fn open(channel: ChannelBroker) -> Self { 17 | Self { 18 | channel, 19 | command_send: false, 20 | } 21 | } 22 | 23 | /// Send an executable command to the server 24 | /// 25 | /// This method is non-block as it will not wait the result 26 | /// 27 | pub fn send_command(&mut self, command: &str) -> SshResult<()> { 28 | if self.command_send { 29 | return Err(SshError::GeneralError( 30 | "An exec channle can only send one command".to_owned(), 31 | )); 32 | } 33 | 34 | tracing::debug!("Send command {}", command); 35 | self.command_send = true; 36 | let mut data = Data::new(); 37 | data.put_u8(ssh_connection_code::CHANNEL_REQUEST) 38 | .put_u32(self.server_channel_no) 39 | .put_str(ssh_str::EXEC) 40 | .put_u8(true as u8) 41 | .put_str(command); 42 | self.send(data) 43 | } 44 | 45 | /// Get the result of the prior command 46 | /// 47 | /// This method will block until the server close the channel 48 | /// 49 | pub fn get_result(&mut self) -> SshResult> { 50 | self.recv_to_end() 51 | } 52 | } 53 | 54 | impl Deref for ExecBroker { 55 | type Target = ChannelBroker; 56 | fn deref(&self) -> &Self::Target { 57 | &self.channel 58 | } 59 | } 60 | 61 | impl DerefMut for ExecBroker { 62 | fn deref_mut(&mut self) -> &mut Self::Target { 63 | &mut self.channel 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/channel/backend/channel_shell.rs: -------------------------------------------------------------------------------- 1 | use super::channel::ChannelBroker; 2 | use crate::constant::{ssh_connection_code, ssh_str}; 3 | use crate::error::SshResult; 4 | use crate::model::Data; 5 | use crate::TerminalSize; 6 | use std::ops::{Deref, DerefMut}; 7 | 8 | pub struct ShellBrocker(ChannelBroker); 9 | 10 | impl ShellBrocker { 11 | pub(crate) fn open(channel: ChannelBroker, tv: TerminalSize) -> SshResult { 12 | // to open a shell channel, we need to request a pesudo-terminal 13 | let mut channel_shell = ShellBrocker(channel); 14 | channel_shell.request_pty(tv)?; 15 | channel_shell.get_shell()?; 16 | Ok(channel_shell) 17 | } 18 | 19 | fn request_pty(&mut self, tv: TerminalSize) -> SshResult<()> { 20 | let tvs = tv.fetch(); 21 | let mut data = Data::new(); 22 | data.put_u8(ssh_connection_code::CHANNEL_REQUEST) 23 | .put_u32(self.server_channel_no) 24 | .put_str(ssh_str::PTY_REQ) 25 | .put_u8(true as u8) 26 | .put_str(ssh_str::XTERM_VAR) 27 | .put_u32(tvs.0) 28 | .put_u32(tvs.1) 29 | .put_u32(tvs.2) 30 | .put_u32(tvs.3); 31 | let model = [ 32 | 128, // TTY_OP_ISPEED 33 | 0, 1, 0xc2, 0, // 115200 34 | 129, // TTY_OP_OSPEED 35 | 0, 1, 0xc2, 0, // 115200 again 36 | 0_u8, // TTY_OP_END 37 | ]; 38 | data.put_u8s(&model); 39 | self.send(data) 40 | } 41 | 42 | fn get_shell(&mut self) -> SshResult<()> { 43 | let mut data = Data::new(); 44 | data.put_u8(ssh_connection_code::CHANNEL_REQUEST) 45 | .put_u32(self.server_channel_no) 46 | .put_str(ssh_str::SHELL) 47 | .put_u8(true as u8); 48 | self.send(data) 49 | } 50 | 51 | /// this method will try to read as much data as we can from the server, 52 | /// but it will block until at least one packet is received 53 | /// 54 | pub fn read(&mut self) -> SshResult> { 55 | let mut out = self.recv()?; 56 | while let Ok(Some(mut data)) = self.try_recv() { 57 | out.append(&mut data) 58 | } 59 | Ok(out) 60 | } 61 | 62 | /// this method send `buf` to the remote pty 63 | /// 64 | pub fn write(&mut self, buf: &[u8]) -> SshResult<()> { 65 | self.send_data(buf.to_vec().into())?; 66 | Ok(()) 67 | } 68 | } 69 | 70 | impl Deref for ShellBrocker { 71 | type Target = ChannelBroker; 72 | fn deref(&self) -> &Self::Target { 73 | &self.0 74 | } 75 | } 76 | 77 | impl DerefMut for ShellBrocker { 78 | fn deref_mut(&mut self) -> &mut Self::Target { 79 | &mut self.0 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/channel/backend/mod.rs: -------------------------------------------------------------------------------- 1 | mod channel; 2 | mod channel_exec; 3 | mod channel_shell; 4 | 5 | pub(crate) use channel::Channel; 6 | pub use channel::ChannelBroker; 7 | pub use channel_exec::ExecBroker; 8 | pub use channel_shell::ShellBrocker; 9 | 10 | #[cfg(feature = "scp")] 11 | mod channel_scp; 12 | #[cfg(feature = "scp")] 13 | pub use channel_scp::ScpBroker; 14 | -------------------------------------------------------------------------------- /src/channel/local/channel_exec.rs: -------------------------------------------------------------------------------- 1 | use super::channel::Channel; 2 | use crate::error::SshResult; 3 | use crate::model::Data; 4 | use crate::{ 5 | constant::{ssh_connection_code, ssh_str}, 6 | SshError, 7 | }; 8 | use std::{ 9 | io::{Read, Write}, 10 | ops::{Deref, DerefMut}, 11 | }; 12 | 13 | pub struct ChannelExec { 14 | channel: Channel, 15 | command_send: bool, 16 | } 17 | 18 | impl ChannelExec 19 | where 20 | S: Read + Write, 21 | { 22 | pub(crate) fn open(channel: Channel) -> Self { 23 | Self { 24 | channel, 25 | command_send: false, 26 | } 27 | } 28 | 29 | /// Send an executable command to the server 30 | /// 31 | pub fn exec_command(&mut self, command: &str) -> SshResult<()> { 32 | if self.command_send { 33 | return Err(SshError::GeneralError( 34 | "An exec channle can only send one command".to_owned(), 35 | )); 36 | } 37 | 38 | tracing::debug!("Send command {}", command); 39 | self.command_send = true; 40 | let mut data = Data::new(); 41 | data.put_u8(ssh_connection_code::CHANNEL_REQUEST) 42 | .put_u32(self.server_channel_no) 43 | .put_str(ssh_str::EXEC) 44 | .put_u8(true as u8) 45 | .put_str(command); 46 | self.send(data) 47 | } 48 | 49 | /// Get the output of the previous command 50 | /// 51 | pub fn get_output(&mut self) -> SshResult> { 52 | let r: Vec = self.recv_to_end()?; 53 | Ok(r) 54 | } 55 | 56 | /// Send an executable command to the server 57 | /// and get the result 58 | /// 59 | /// This method also implicitly consume the channel object, 60 | /// since the exec channel can only execute one command 61 | /// 62 | pub fn send_command(mut self, command: &str) -> SshResult> { 63 | self.exec_command(command)?; 64 | 65 | self.get_output() 66 | } 67 | } 68 | 69 | impl Deref for ChannelExec 70 | where 71 | S: Read + Write, 72 | { 73 | type Target = Channel; 74 | fn deref(&self) -> &Self::Target { 75 | &self.channel 76 | } 77 | } 78 | 79 | impl DerefMut for ChannelExec 80 | where 81 | S: Read + Write, 82 | { 83 | fn deref_mut(&mut self) -> &mut Self::Target { 84 | &mut self.channel 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/channel/local/channel_shell.rs: -------------------------------------------------------------------------------- 1 | use super::channel::Channel; 2 | use crate::constant::{ssh_connection_code, ssh_str}; 3 | use crate::error::SshResult; 4 | use crate::model::{Data, TerminalSize}; 5 | use std::{ 6 | io::{Read, Write}, 7 | ops::{Deref, DerefMut}, 8 | }; 9 | 10 | pub struct ChannelShell(Channel); 11 | 12 | impl ChannelShell 13 | where 14 | S: Read + Write, 15 | { 16 | pub(crate) fn open(channel: Channel, tv: TerminalSize) -> SshResult { 17 | // to open a shell channel, we need to request a pesudo-terminal 18 | let mut channel_shell = ChannelShell(channel); 19 | channel_shell.request_pty(tv)?; 20 | channel_shell.get_shell()?; 21 | Ok(channel_shell) 22 | } 23 | 24 | fn request_pty(&mut self, tv: TerminalSize) -> SshResult<()> { 25 | let tvs = tv.fetch(); 26 | let mut data = Data::new(); 27 | data.put_u8(ssh_connection_code::CHANNEL_REQUEST) 28 | .put_u32(self.server_channel_no) 29 | .put_str(ssh_str::PTY_REQ) 30 | .put_u8(false as u8) 31 | .put_str(ssh_str::XTERM_VAR) 32 | .put_u32(tvs.0) 33 | .put_u32(tvs.1) 34 | .put_u32(tvs.2) 35 | .put_u32(tvs.3); 36 | let model = [ 37 | 128, // TTY_OP_ISPEED 38 | 0, 1, 0xc2, 0, // 115200 39 | 129, // TTY_OP_OSPEED 40 | 0, 1, 0xc2, 0, // 115200 again 41 | 0_u8, // TTY_OP_END 42 | ]; 43 | data.put_u8s(&model); 44 | self.send(data) 45 | } 46 | 47 | fn get_shell(&mut self) -> SshResult<()> { 48 | let mut data = Data::new(); 49 | data.put_u8(ssh_connection_code::CHANNEL_REQUEST) 50 | .put_u32(self.server_channel_no) 51 | .put_str(ssh_str::SHELL) 52 | .put_u8(false as u8); 53 | self.send(data) 54 | } 55 | 56 | /// this method will try to read as much data as we can from the server, 57 | /// but it will block until at least one packet is received 58 | /// 59 | pub fn read(&mut self) -> SshResult> { 60 | let mut out = self.recv()?; 61 | while let Ok(Some(mut data)) = self.try_recv() { 62 | out.append(&mut data) 63 | } 64 | Ok(out) 65 | } 66 | 67 | /// this method send `buf` to the remote pty 68 | /// 69 | pub fn write(&mut self, buf: &[u8]) -> SshResult<()> { 70 | let _ = self.send_data(buf.to_vec())?; 71 | Ok(()) 72 | } 73 | } 74 | 75 | impl Deref for ChannelShell 76 | where 77 | S: Read + Write, 78 | { 79 | type Target = Channel; 80 | fn deref(&self) -> &Self::Target { 81 | &self.0 82 | } 83 | } 84 | 85 | impl DerefMut for ChannelShell 86 | where 87 | S: Read + Write, 88 | { 89 | fn deref_mut(&mut self) -> &mut Self::Target { 90 | &mut self.0 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/channel/local/mod.rs: -------------------------------------------------------------------------------- 1 | mod channel; 2 | mod channel_exec; 3 | mod channel_shell; 4 | 5 | pub use channel::Channel; 6 | pub use channel_exec::ChannelExec; 7 | pub use channel_shell::ChannelShell; 8 | 9 | #[cfg(feature = "scp")] 10 | mod channel_scp; 11 | #[cfg(feature = "scp")] 12 | pub use channel_scp::ChannelScp; 13 | -------------------------------------------------------------------------------- /src/channel/mod.rs: -------------------------------------------------------------------------------- 1 | mod backend; 2 | mod local; 3 | 4 | pub(crate) use backend::Channel as BackendChannel; 5 | pub use backend::{ChannelBroker, ExecBroker, ShellBrocker}; 6 | 7 | pub use local::Channel as LocalChannel; 8 | pub use local::ChannelExec as LocalExec; 9 | pub use local::ChannelShell as LocalShell; 10 | 11 | #[cfg(feature = "scp")] 12 | pub use backend::ScpBroker; 13 | #[cfg(feature = "scp")] 14 | pub use local::ChannelScp as LocalScp; 15 | -------------------------------------------------------------------------------- /src/client/client.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | algorithm::compression::{CompressNone, Compression}, 3 | config::algorithm::AlgList, 4 | }; 5 | use crate::{algorithm::encryption::Encryption, config::Config}; 6 | use crate::{algorithm::encryption::EncryptionNone, model::Sequence}; 7 | use std::time::Duration; 8 | 9 | // the underlay connection 10 | pub(crate) struct Client { 11 | pub(super) sequence: Sequence, 12 | pub(super) config: Config, 13 | pub(super) negotiated: AlgList, 14 | pub(super) encryptor: Box, 15 | pub(super) compressor: Box, 16 | pub(super) session_id: Vec, 17 | } 18 | 19 | impl Client { 20 | pub fn new(config: Config) -> Self { 21 | Self { 22 | config, 23 | encryptor: Box::::default(), 24 | compressor: Box::::default(), 25 | negotiated: AlgList::new(), 26 | session_id: vec![], 27 | sequence: Sequence::new(), 28 | } 29 | } 30 | 31 | pub fn get_encryptor(&mut self) -> &mut dyn Encryption { 32 | self.encryptor.as_mut() 33 | } 34 | 35 | pub fn get_compressor(&mut self) -> &mut dyn Compression { 36 | self.compressor.as_mut() 37 | } 38 | 39 | pub fn get_seq(&mut self) -> &mut Sequence { 40 | &mut self.sequence 41 | } 42 | 43 | pub fn get_timeout(&self) -> Option { 44 | self.config.timeout 45 | } 46 | 47 | pub fn set_timeout(&mut self, tm: Option) { 48 | self.config.timeout = tm 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/client/client_auth.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use tracing::*; 3 | 4 | use crate::{ 5 | algorithm::{compression, Compress, Digest}, 6 | constant::{ssh_connection_code, ssh_str, ssh_transport_code, ssh_user_auth_code}, 7 | error::{SshError, SshResult}, 8 | model::{Data, Packet, SecPacket}, 9 | }; 10 | 11 | use super::Client; 12 | 13 | impl Client { 14 | pub fn do_auth(&mut self, stream: &mut S, digest: &Digest) -> SshResult<()> 15 | where 16 | S: Read + Write, 17 | { 18 | info!("Auth start"); 19 | let mut data = Data::new(); 20 | data.put_u8(ssh_transport_code::SERVICE_REQUEST) 21 | .put_str(ssh_str::SSH_USERAUTH); 22 | data.pack(self).write_stream(stream)?; 23 | 24 | let mut tried_public_key = false; 25 | loop { 26 | let mut data = Data::unpack(SecPacket::from_stream(stream, self)?)?; 27 | let message_code = data.get_u8(); 28 | match message_code { 29 | ssh_transport_code::SERVICE_ACCEPT => { 30 | if self.config.auth.key_pair.is_none() { 31 | tried_public_key = true; 32 | // if no private key specified 33 | // just try password auth 34 | self.password_authentication(stream)? 35 | } else { 36 | // if private key was provided 37 | // use public key auth first, then fallback to password auth 38 | self.public_key_authentication(stream)? 39 | } 40 | } 41 | ssh_user_auth_code::FAILURE => { 42 | if !tried_public_key { 43 | error!("user auth failure. (public key)"); 44 | info!("fallback to password authentication"); 45 | tried_public_key = true; 46 | // keep the same with openssh 47 | // if the public key auth failed 48 | // try with password again 49 | self.password_authentication(stream)? 50 | } else { 51 | error!("user auth failure. (password)"); 52 | return Err(SshError::AuthError); 53 | } 54 | } 55 | ssh_user_auth_code::PK_OK => { 56 | info!("user auth support this algorithm."); 57 | self.public_key_signature(stream, digest)? 58 | } 59 | ssh_user_auth_code::SUCCESS => { 60 | info!("user auth successful."); 61 | // 62 | // Now we need turn on the compressor if any 63 | if let Compress::ZlibOpenSsh = self.negotiated.c_compress[0] { 64 | let comp = compression::from(&Compress::ZlibOpenSsh); 65 | self.compressor = comp; 66 | } 67 | return Ok(()); 68 | } 69 | ssh_connection_code::GLOBAL_REQUEST => { 70 | let mut data = Data::new(); 71 | data.put_u8(ssh_connection_code::REQUEST_FAILURE); 72 | data.pack(self).write_stream(stream)?; 73 | } 74 | _ => {} 75 | } 76 | } 77 | } 78 | 79 | fn password_authentication(&mut self, stream: &mut S) -> SshResult<()> 80 | where 81 | S: Write, 82 | { 83 | info!("password authentication."); 84 | let mut data = Data::new(); 85 | data.put_u8(ssh_user_auth_code::REQUEST) 86 | .put_str(self.config.auth.username.as_str()) 87 | .put_str(ssh_str::SSH_CONNECTION) 88 | .put_str(ssh_str::PASSWORD) 89 | .put_u8(false as u8) 90 | .put_str(self.config.auth.password.as_str()); 91 | 92 | data.pack(self).write_stream(stream) 93 | } 94 | 95 | fn public_key_authentication(&mut self, stream: &mut S) -> SshResult<()> 96 | where 97 | S: Write, 98 | { 99 | let data = { 100 | let pubkey_alg = &self.negotiated.public_key[0]; 101 | info!( 102 | "public key authentication. algorithm: {}", 103 | pubkey_alg.as_ref() 104 | ); 105 | let mut data = Data::new(); 106 | data.put_u8(ssh_user_auth_code::REQUEST) 107 | .put_str(self.config.auth.username.as_str()) 108 | .put_str(ssh_str::SSH_CONNECTION) 109 | .put_str(ssh_str::PUBLIC_KEY) 110 | .put_u8(false as u8) 111 | .put_str(pubkey_alg.as_ref()) 112 | .put_u8s( 113 | &self 114 | .config 115 | .auth 116 | .key_pair 117 | .as_ref() 118 | .unwrap() 119 | .get_blob(pubkey_alg), 120 | ); 121 | data 122 | }; 123 | data.pack(self).write_stream(stream) 124 | } 125 | 126 | pub(crate) fn public_key_signature( 127 | &mut self, 128 | stream: &mut S, 129 | digest: &Digest, 130 | ) -> SshResult<()> 131 | where 132 | S: Write, 133 | { 134 | let data = { 135 | let pubkey_alg = &self.negotiated.public_key[0]; 136 | 137 | let mut data = Data::new(); 138 | data.put_u8(ssh_user_auth_code::REQUEST) 139 | .put_str(self.config.auth.username.as_str()) 140 | .put_str(ssh_str::SSH_CONNECTION) 141 | .put_str(ssh_str::PUBLIC_KEY) 142 | .put_u8(true as u8) 143 | .put_str(pubkey_alg.as_ref()) 144 | .put_u8s( 145 | &self 146 | .config 147 | .auth 148 | .key_pair 149 | .as_ref() 150 | .unwrap() 151 | .get_blob(pubkey_alg), 152 | ); 153 | let signature = self.config.auth.key_pair.as_ref().unwrap().signature( 154 | data.as_slice(), 155 | digest.hash_ctx.clone(), 156 | digest.key_exchange.as_ref().unwrap().get_hash_type(), 157 | pubkey_alg, 158 | ); 159 | data.put_u8s(&signature); 160 | data 161 | }; 162 | data.pack(self).write_stream(stream) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/client/client_kex.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "deprecated-zlib")] 2 | use crate::algorithm::{compression, Compress}; 3 | use crate::{ 4 | algorithm::{ 5 | encryption, 6 | hash::{self, HashCtx}, 7 | key_exchange::{self, KeyExchange}, 8 | mac, 9 | public_key::{self, PublicKey}, 10 | Digest, 11 | }, 12 | client::Client, 13 | config::algorithm::AlgList, 14 | constant::ssh_transport_code, 15 | error::{SshError, SshResult}, 16 | model::{Data, Packet, SecPacket}, 17 | }; 18 | use std::io::{Read, Write}; 19 | use tracing::*; 20 | 21 | impl Client { 22 | pub fn key_agreement( 23 | &mut self, 24 | stream: &mut S, 25 | server_algs: AlgList, 26 | digest: &mut Digest, 27 | ) -> SshResult<()> 28 | where 29 | S: Read + Write, 30 | { 31 | // initialize the hash context 32 | digest.hash_ctx.set_v_c(&self.config.ver.client_ver); 33 | digest.hash_ctx.set_v_s(&self.config.ver.server_ver); 34 | 35 | info!("start for key negotiation."); 36 | info!("send client algorithm list."); 37 | 38 | let algs = self.config.algs.clone(); 39 | let client_algs = algs.pack(self); 40 | digest.hash_ctx.set_i_c(client_algs.get_inner()); 41 | client_algs.write_stream(stream)?; 42 | 43 | let negotiated = self.config.algs.match_with(&server_algs)?; 44 | 45 | // key exchange algorithm 46 | let mut key_exchange = key_exchange::from(&negotiated.key_exchange[0])?; 47 | self.send_qc(stream, key_exchange.get_public_key())?; 48 | 49 | // host key algorithm 50 | let mut public_key = public_key::from(&negotiated.public_key[0]); 51 | 52 | // generate session id 53 | let session_id = { 54 | let session_id = self.verify_signature_and_new_keys( 55 | stream, 56 | &mut public_key, 57 | &mut key_exchange, 58 | &mut digest.hash_ctx, 59 | )?; 60 | 61 | if self.session_id.is_empty() { 62 | session_id 63 | } else { 64 | self.session_id.clone() 65 | } 66 | }; 67 | 68 | let hash = hash::Hash::new( 69 | digest.hash_ctx.clone(), 70 | &session_id, 71 | key_exchange.get_hash_type(), 72 | ); 73 | 74 | // mac algorithm 75 | let mac = mac::from(&negotiated.c_mac[0]); 76 | 77 | // encryption algorithm 78 | let encryption = encryption::from(&negotiated.c_encryption[0], hash, mac); 79 | 80 | self.session_id = session_id; 81 | self.negotiated = negotiated; 82 | self.encryptor = encryption; 83 | 84 | #[cfg(feature = "deprecated-zlib")] 85 | { 86 | if let Compress::Zlib = self.negotiated.c_compress[0] { 87 | let comp = compression::from(&Compress::Zlib); 88 | self.compressor = comp; 89 | } 90 | } 91 | 92 | digest.key_exchange = Some(key_exchange); 93 | 94 | info!("key negotiation successful."); 95 | 96 | Ok(()) 97 | } 98 | 99 | /// Send the public key 100 | fn send_qc(&mut self, stream: &mut S, public_key: &[u8]) -> SshResult<()> 101 | where 102 | S: Read + Write, 103 | { 104 | let mut data = Data::new(); 105 | data.put_u8(ssh_transport_code::KEXDH_INIT) 106 | .put_u8s(public_key); 107 | data.pack(self).write_stream(stream) 108 | } 109 | 110 | fn verify_signature_and_new_keys( 111 | &mut self, 112 | stream: &mut S, 113 | public_key: &mut Box, 114 | key_exchange: &mut Box, 115 | h: &mut HashCtx, 116 | ) -> SshResult> 117 | where 118 | S: Read + Write, 119 | { 120 | let mut session_id = vec![]; 121 | loop { 122 | let mut data = Data::unpack(SecPacket::from_stream(stream, self)?)?; 123 | let message_code = data.get_u8(); 124 | match message_code { 125 | ssh_transport_code::KEXDH_REPLY => { 126 | // Generate the session id, get the signature 127 | let sig = self.generate_signature(data, h, key_exchange)?; 128 | // verify the signature 129 | session_id = hash::digest(&h.as_bytes(), key_exchange.get_hash_type()); 130 | let flag = public_key.verify_signature(&h.k_s, &session_id, &sig)?; 131 | if !flag { 132 | let err_msg = "signature verification failure.".to_owned(); 133 | error!(err_msg); 134 | return Err(SshError::KexError(err_msg)); 135 | } 136 | info!("signature verification success."); 137 | } 138 | ssh_transport_code::NEWKEYS => { 139 | self.new_keys(stream)?; 140 | return Ok(session_id); 141 | } 142 | _ => unreachable!(), 143 | } 144 | } 145 | } 146 | 147 | /// get the signature 148 | fn generate_signature( 149 | &mut self, 150 | mut data: Data, 151 | h: &mut HashCtx, 152 | key_exchange: &mut Box, 153 | ) -> SshResult> { 154 | let ks = data.get_u8s(); 155 | h.set_k_s(&ks); 156 | // TODO: 157 | // No fingerprint verification 158 | let qs = data.get_u8s(); 159 | h.set_e(key_exchange.get_public_key()); 160 | h.set_f(&qs); 161 | let vec = key_exchange.get_shared_secret(qs)?; 162 | h.set_k(&vec); 163 | let h = data.get_u8s(); 164 | let mut hd = Data::from(h); 165 | hd.get_u8s(); 166 | let signature = hd.get_u8s(); 167 | Ok(signature) 168 | } 169 | 170 | /// NEWKEYS indicates that kex is done 171 | fn new_keys(&mut self, stream: &mut S) -> SshResult<()> 172 | where 173 | S: Write, 174 | { 175 | let mut data = Data::new(); 176 | data.put_u8(ssh_transport_code::NEWKEYS); 177 | info!("send new keys"); 178 | data.pack(self).write_stream(stream) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::module_inception)] 2 | pub(crate) mod client; 3 | mod client_auth; 4 | mod client_kex; 5 | 6 | pub(crate) use client::Client; 7 | -------------------------------------------------------------------------------- /src/config/algorithm.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{Debug, Display}, 3 | ops::{Deref, DerefMut}, 4 | str::FromStr, 5 | }; 6 | use tracing::*; 7 | 8 | use crate::{ 9 | algorithm::{Compress, Enc, Kex, Mac, PubKey}, 10 | client::Client, 11 | constant::ssh_transport_code, 12 | error::{SshError, SshResult}, 13 | model::{Data, Packet, SecPacket}, 14 | util, 15 | }; 16 | 17 | macro_rules! create_wrapped_type { 18 | ($name: ident, $value_type: ty) => { 19 | #[derive(Clone, Default)] 20 | pub(crate) struct $name(Vec<$value_type>); 21 | impl Deref for $name { 22 | type Target = Vec<$value_type>; 23 | fn deref(&self) -> &Self::Target { 24 | &self.0 25 | } 26 | } 27 | 28 | impl DerefMut for $name { 29 | fn deref_mut(&mut self) -> &mut Self::Target { 30 | &mut self.0 31 | } 32 | } 33 | 34 | impl Display for $name { 35 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 36 | write!( 37 | f, 38 | "{}", 39 | self.iter() 40 | .map(|&x| x.as_ref().to_owned()) 41 | .collect::>() 42 | .join(",") 43 | ) 44 | } 45 | } 46 | 47 | impl TryFrom> for $name { 48 | type Error = SshError; 49 | fn try_from(v: Vec) -> Result { 50 | let v = v 51 | .iter() 52 | .filter_map(|x| <$value_type>::from_str(x.as_str()).ok()) 53 | .collect::>(); 54 | Ok(Self(v)) 55 | } 56 | } 57 | 58 | impl From> for $name { 59 | fn from(v: Vec<$value_type>) -> Self { 60 | Self(v) 61 | } 62 | } 63 | }; 64 | } 65 | 66 | create_wrapped_type!(Kexs, Kex); 67 | create_wrapped_type!(PubKeys, PubKey); 68 | create_wrapped_type!(Encs, Enc); 69 | create_wrapped_type!(Macs, Mac); 70 | create_wrapped_type!(Compresses, Compress); 71 | 72 | #[derive(Clone, Default)] 73 | pub(crate) struct AlgList { 74 | pub key_exchange: Kexs, 75 | pub public_key: PubKeys, 76 | pub c_encryption: Encs, 77 | pub s_encryption: Encs, 78 | pub c_mac: Macs, 79 | pub s_mac: Macs, 80 | pub c_compress: Compresses, 81 | pub s_compress: Compresses, 82 | } 83 | 84 | impl Debug for AlgList { 85 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 86 | write!(f, "kex: \"{}\", ", self.key_exchange)?; 87 | write!(f, "pubkey: \"{}\", ", self.public_key)?; 88 | write!(f, "c_enc: \"{}\", ", self.c_encryption)?; 89 | write!(f, "s_enc: \"{}\", ", self.s_encryption)?; 90 | write!(f, "c_mac: \"{}\", ", self.c_mac)?; 91 | write!(f, "s_mac: \"{}\", ", self.s_mac)?; 92 | write!(f, "c_compress: \"{}\", ", self.c_compress)?; 93 | write!(f, "s_compress: \"{}\"", self.s_compress) 94 | } 95 | } 96 | 97 | impl AlgList { 98 | pub fn new() -> Self { 99 | AlgList { 100 | ..Default::default() 101 | } 102 | } 103 | 104 | pub fn client_default() -> Self { 105 | AlgList { 106 | key_exchange: vec![ 107 | Kex::Curve25519Sha256, 108 | Kex::EcdhSha2Nistrp256, 109 | Kex::DiffieHellmanGroup14Sha256, 110 | Kex::DiffieHellmanGroup14Sha1, 111 | ] 112 | .into(), 113 | public_key: vec![PubKey::RsaSha2_512, PubKey::RsaSha2_256].into(), 114 | c_encryption: vec![ 115 | Enc::Chacha20Poly1305Openssh, 116 | Enc::Aes128Ctr, 117 | Enc::Aes192Ctr, 118 | Enc::Aes256Ctr, 119 | ] 120 | .into(), 121 | s_encryption: vec![ 122 | Enc::Chacha20Poly1305Openssh, 123 | Enc::Aes128Ctr, 124 | Enc::Aes192Ctr, 125 | Enc::Aes256Ctr, 126 | ] 127 | .into(), 128 | c_mac: vec![Mac::HmacSha2_256, Mac::HmacSha2_512, Mac::HmacSha1].into(), 129 | s_mac: vec![Mac::HmacSha2_256, Mac::HmacSha2_512, Mac::HmacSha1].into(), 130 | c_compress: vec![Compress::None, Compress::ZlibOpenSsh].into(), 131 | s_compress: vec![Compress::None, Compress::ZlibOpenSsh].into(), 132 | } 133 | } 134 | 135 | fn from(mut data: Data) -> SshResult { 136 | data.get_u8(); 137 | // skip the 16-bit cookie 138 | data.skip(16); 139 | let mut server_algorithm = Self::new(); 140 | 141 | macro_rules! try_convert { 142 | ($hint: literal, $field: ident) => { 143 | let alg_string = util::vec_u8_to_string(data.get_u8s(), ",")?; 144 | info!("server {}: {:?}", $hint, alg_string); 145 | server_algorithm.$field = alg_string.try_into()?; 146 | }; 147 | } 148 | try_convert!("key exchange", key_exchange); 149 | try_convert!("public key", public_key); 150 | try_convert!("c2s encryption", c_encryption); 151 | try_convert!("s2c encryption", s_encryption); 152 | try_convert!("c2s mac", c_mac); 153 | try_convert!("s2c mac", s_mac); 154 | try_convert!("c2s compression", c_compress); 155 | try_convert!("s2c compression", s_compress); 156 | debug!("converted server algorithms: [{:?}]", server_algorithm); 157 | Ok(server_algorithm) 158 | } 159 | 160 | pub fn match_with(&self, other: &Self) -> SshResult { 161 | macro_rules! match_field { 162 | ($our: expr, $their:expr, $field: ident, $err_hint: literal) => { 163 | $our.$field 164 | .iter() 165 | .find_map(|k| { 166 | if $their.$field.contains(k) { 167 | Some(k) 168 | } else { 169 | None 170 | } 171 | }) 172 | .ok_or_else(|| { 173 | let err_msg = format!( 174 | "Key_agreement: the {} fails to match, \ 175 | algorithms supported by the server: {},\ 176 | algorithms supported by the client: {}", 177 | $err_hint, $their.$field, $our.$field 178 | ); 179 | error!(err_msg); 180 | SshError::KexError(err_msg) 181 | }) 182 | }; 183 | } 184 | 185 | // kex 186 | let kex = match_field!(self, other, key_exchange, "DH algorithm")?; 187 | // pubkey 188 | let pubkey = match_field!(self, other, public_key, "signature algorithm")?; 189 | // encryption 190 | let c_enc = match_field!(self, other, c_encryption, "client encryption algorithm")?; 191 | let s_enc = match_field!(self, other, s_encryption, "server encryption algorithm")?; 192 | 193 | // mac 194 | let c_mac = match_field!(self, other, c_mac, "client mac algorithm")?; 195 | let s_mac = match_field!(self, other, s_mac, "server mac algorithm")?; 196 | 197 | // compress 198 | let c_compress = match_field!(self, other, c_compress, "client compression algorithm")?; 199 | let s_compress = match_field!(self, other, s_compress, "server compression algorithm")?; 200 | 201 | let negotiated = Self { 202 | key_exchange: vec![*kex].into(), 203 | public_key: vec![*pubkey].into(), 204 | c_encryption: vec![*c_enc].into(), 205 | s_encryption: vec![*s_enc].into(), 206 | c_mac: vec![*c_mac].into(), 207 | s_mac: vec![*s_mac].into(), 208 | c_compress: vec![*c_compress].into(), 209 | s_compress: vec![*s_compress].into(), 210 | }; 211 | 212 | info!("matched algorithms [{:?}]", negotiated); 213 | 214 | Ok(negotiated) 215 | } 216 | 217 | fn as_i(&self) -> Vec { 218 | let mut data = Data::new(); 219 | data.put_str(&self.key_exchange.to_string()); 220 | data.put_str(&self.public_key.to_string()); 221 | data.put_str(&self.c_encryption.to_string()); 222 | data.put_str(&self.s_encryption.to_string()); 223 | data.put_str(&self.c_mac.to_string()); 224 | data.put_str(&self.s_mac.to_string()); 225 | data.put_str(&self.c_compress.to_string()); 226 | data.put_str(&self.s_compress.to_string()); 227 | data.to_vec() 228 | } 229 | } 230 | 231 | impl<'a> Packet<'a> for AlgList { 232 | fn pack(self, client: &'a mut Client) -> crate::model::SecPacket<'a> { 233 | info!("client algorithms: [{:?}]", self); 234 | let mut data = Data::new(); 235 | data.put_u8(ssh_transport_code::KEXINIT); 236 | data.extend(util::cookie()); 237 | data.extend(self.as_i()); 238 | data.put_str("") 239 | .put_str("") 240 | .put_u8(false as u8) 241 | .put_u32(0_u32); 242 | 243 | (data, client).into() 244 | } 245 | 246 | fn unpack(pkt: SecPacket) -> SshResult 247 | where 248 | Self: Sized, 249 | { 250 | let data = pkt.into_inner(); 251 | assert_eq!(data[0], ssh_transport_code::KEXINIT); 252 | AlgList::from(data) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/config/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::algorithm::{ 2 | hash::{self, HashCtx, HashType}, 3 | PubKey, 4 | }; 5 | use crate::model::Data; 6 | use crate::{SshError, SshResult}; 7 | use rsa::pkcs1::DecodeRsaPrivateKey; 8 | use rsa::pkcs1v15::Pkcs1v15Sign; 9 | use rsa::traits::PublicKeyParts; 10 | use std::fmt::Debug; 11 | use std::fs::File; 12 | use std::io::Read; 13 | use std::path::Path; 14 | 15 | const KEY_FILE_MAGIC_START: &str = "-----BEGIN OPENSSH PRIVATE KEY-----"; 16 | 17 | #[derive(Clone, Default)] 18 | pub struct KeyPair { 19 | pub(super) private_key: String, 20 | pub(super) key_type: KeyType, 21 | } 22 | 23 | impl KeyPair { 24 | pub fn from_str(key_str: &str) -> SshResult { 25 | // first validate the key 26 | let key_str = key_str.trim().to_owned(); 27 | 28 | let (key_type, private_key) = if rsa::RsaPrivateKey::from_pkcs1_pem(&key_str).is_ok() { 29 | (KeyType::PemRsa, key_str) 30 | } else if key_str.starts_with(KEY_FILE_MAGIC_START) { 31 | match ssh_key::PrivateKey::from_openssh(&key_str) { 32 | Ok(prk) => match prk.algorithm() { 33 | ssh_key::Algorithm::Rsa { hash: _hash } => (KeyType::SshRsa, key_str), 34 | ssh_key::Algorithm::Ed25519 => (KeyType::SshEd25519, key_str), 35 | x => { 36 | return Err(SshError::SshPubKeyError(format!( 37 | "Currently don't support the key file type {}", 38 | x 39 | ))) 40 | } 41 | }, 42 | Err(e) => return Err(SshError::SshPubKeyError(e.to_string())), 43 | } 44 | } else { 45 | return Err(SshError::SshPubKeyError( 46 | "Unable to detect the pulic key type".to_owned(), 47 | )); 48 | }; 49 | 50 | // then store it 51 | let pair = KeyPair { 52 | private_key, 53 | key_type, 54 | }; 55 | Ok(pair) 56 | } 57 | 58 | pub fn get_blob(&self, alg: &PubKey) -> Vec { 59 | match self.key_type { 60 | KeyType::PemRsa => { 61 | // already valid key string, just unwrap it. 62 | let rprk = rsa::RsaPrivateKey::from_pkcs1_pem(&self.private_key).unwrap(); 63 | let rpuk = rprk.to_public_key(); 64 | let es = rpuk.e().to_bytes_be(); 65 | let ns = rpuk.n().to_bytes_be(); 66 | let mut blob = Data::new(); 67 | blob.put_str(alg.as_ref()); 68 | blob.put_mpint(&es); 69 | blob.put_mpint(&ns); 70 | blob.to_vec() 71 | } 72 | KeyType::SshRsa => { 73 | let prk = ssh_key::PrivateKey::from_openssh(&self.private_key).unwrap(); 74 | let rsa = prk.key_data().rsa().unwrap(); 75 | let es = rsa.public.e.as_bytes(); 76 | let ns = rsa.public.n.as_bytes(); 77 | let mut blob = Data::new(); 78 | blob.put_str(alg.as_ref()); 79 | blob.put_mpint(es); 80 | blob.put_mpint(ns); 81 | blob.to_vec() 82 | } 83 | KeyType::SshEd25519 => { 84 | let prk = ssh_key::PrivateKey::from_openssh(&self.private_key).unwrap(); 85 | let ed25519 = prk.key_data().ed25519().unwrap(); 86 | let mut blob = Data::new(); 87 | blob.put_str(alg.as_ref()); 88 | blob.put_u8s(ed25519.public.as_ref()); 89 | blob.to_vec() 90 | } 91 | } 92 | } 93 | 94 | fn sign(&self, sd: &[u8], alg: &PubKey) -> Vec { 95 | match self.key_type { 96 | KeyType::PemRsa | KeyType::SshRsa => { 97 | let (scheme, digest) = match alg { 98 | PubKey::RsaSha2_512 => ( 99 | Pkcs1v15Sign::new::(), 100 | ring::digest::digest(&ring::digest::SHA512, sd), 101 | ), 102 | PubKey::RsaSha2_256 => ( 103 | Pkcs1v15Sign::new::(), 104 | ring::digest::digest(&ring::digest::SHA256, sd), 105 | ), 106 | #[cfg(feature = "deprecated-rsa-sha1")] 107 | PubKey::SshRsa => ( 108 | Pkcs1v15Sign::new::(), 109 | ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, sd), 110 | ), 111 | _ => unreachable!(), 112 | }; 113 | 114 | let msg = digest.as_ref(); 115 | let rprk = match self.key_type { 116 | KeyType::PemRsa => { 117 | rsa::RsaPrivateKey::from_pkcs1_pem(self.private_key.as_str()).unwrap() 118 | } 119 | KeyType::SshRsa => { 120 | // the sign method in ssh_key itself can only work with sha2-512 121 | // so we convert it to the raw rsa key 122 | let prk = ssh_key::PrivateKey::from_openssh(&self.private_key).unwrap(); 123 | let rsa = prk.key_data().rsa().unwrap(); 124 | rsa::RsaPrivateKey::try_from(rsa).unwrap() 125 | } 126 | _ => unreachable!(), 127 | }; 128 | 129 | rprk.sign(scheme, msg).unwrap() 130 | } 131 | KeyType::SshEd25519 => { 132 | use signature::Signer; 133 | let prk = ssh_key::PrivateKey::from_openssh(&self.private_key).unwrap(); 134 | let sign = prk.try_sign(sd).unwrap(); 135 | sign.as_bytes().to_vec() 136 | } 137 | } 138 | } 139 | 140 | pub(crate) fn signature( 141 | &self, 142 | buf: &[u8], 143 | hash_ctx: HashCtx, 144 | hash_type: HashType, 145 | alg: &PubKey, 146 | ) -> Vec { 147 | let session_id = hash::digest(hash_ctx.as_bytes().as_slice(), hash_type); 148 | let mut sd = Data::new(); 149 | sd.put_u8s(session_id.as_slice()); 150 | sd.extend_from_slice(buf); 151 | let sign = self.sign(&sd, alg); 152 | let mut ss = Data::new(); 153 | ss.put_str(alg.as_ref()); 154 | ss.put_u8s(&sign); 155 | ss.to_vec() 156 | } 157 | } 158 | 159 | #[derive(Clone, Default)] 160 | pub(super) enum KeyType { 161 | #[default] 162 | PemRsa, 163 | SshRsa, 164 | SshEd25519, 165 | } 166 | 167 | #[derive(Clone, Default)] 168 | pub(crate) struct AuthInfo { 169 | pub username: String, 170 | pub password: String, 171 | pub key_pair: Option, 172 | } 173 | 174 | impl Debug for AuthInfo { 175 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 176 | write!(f, "username: {}", self.username)?; 177 | Ok(()) 178 | } 179 | } 180 | 181 | impl AuthInfo { 182 | pub fn username(&mut self, u: U) -> SshResult<()> 183 | where 184 | U: ToString, 185 | { 186 | self.username = u.to_string(); 187 | Ok(()) 188 | } 189 | 190 | pub fn password

(&mut self, p: P) -> SshResult<()> 191 | where 192 | P: ToString, 193 | { 194 | self.password = p.to_string(); 195 | Ok(()) 196 | } 197 | 198 | pub fn private_key(&mut self, k: K) -> SshResult<()> 199 | where 200 | K: ToString, 201 | { 202 | self.key_pair = Some((KeyPair::from_str(&k.to_string()))?); 203 | Ok(()) 204 | } 205 | 206 | pub fn private_key_path

(&mut self, p: P) -> SshResult<()> 207 | where 208 | P: AsRef, 209 | { 210 | let mut file = File::open(p)?; 211 | let mut prks = String::new(); 212 | file.read_to_string(&mut prks)?; 213 | 214 | self.key_pair = Some((KeyPair::from_str(&prks))?); 215 | Ok(()) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod algorithm; 2 | pub(crate) mod auth; 3 | pub(crate) mod version; 4 | use crate::algorithm::PubKey as PubKeyAlgs; 5 | use std::time::Duration; 6 | 7 | fn insert_or_move_first(v: &mut Vec, alg: PubKeyAlgs) { 8 | if let Some(i) = v.iter().position(|each| *each == alg) { 9 | v.swap(0, i) 10 | } else { 11 | v.insert(0, alg) 12 | } 13 | } 14 | 15 | #[derive(Clone)] 16 | pub(crate) struct Config { 17 | pub ver: version::SshVersion, 18 | pub auth: auth::AuthInfo, 19 | pub algs: algorithm::AlgList, 20 | pub timeout: Option, 21 | auto_tune: bool, 22 | } 23 | 24 | impl Default for Config { 25 | fn default() -> Self { 26 | Self { 27 | algs: algorithm::AlgList::client_default(), 28 | auth: auth::AuthInfo::default(), 29 | ver: version::SshVersion::default(), 30 | timeout: Some(Duration::from_secs(30)), 31 | auto_tune: true, 32 | } 33 | } 34 | } 35 | 36 | impl Config { 37 | // use an empty client algorithm list 38 | pub fn disable_default() -> Self { 39 | Self { 40 | algs: algorithm::AlgList::default(), 41 | auth: auth::AuthInfo::default(), 42 | ver: version::SshVersion::default(), 43 | timeout: Some(Duration::from_secs(30)), 44 | auto_tune: false, 45 | } 46 | } 47 | 48 | pub(crate) fn tune_alglist_on_private_key(&mut self) { 49 | if !self.auto_tune { 50 | return; 51 | } 52 | 53 | if let Some(ref key_pair) = self.auth.key_pair { 54 | match key_pair.key_type { 55 | auth::KeyType::PemRsa | auth::KeyType::SshRsa => { 56 | let pubkeys = &mut self.algs.public_key; 57 | insert_or_move_first(pubkeys, PubKeyAlgs::RsaSha2_256); 58 | insert_or_move_first(pubkeys, PubKeyAlgs::RsaSha2_512); 59 | } 60 | auth::KeyType::SshEd25519 => { 61 | let pubkeys = &mut self.algs.public_key; 62 | insert_or_move_first(pubkeys, PubKeyAlgs::SshEd25519); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/config/version.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::time::Duration; 3 | use tracing::*; 4 | 5 | use crate::{ 6 | constant::{self, CLIENT_VERSION, SSH_MAGIC}, 7 | error::{SshError, SshResult}, 8 | model::Timeout, 9 | }; 10 | 11 | #[derive(Debug, Clone)] 12 | pub(crate) struct SshVersion { 13 | pub client_ver: String, 14 | pub server_ver: String, 15 | } 16 | 17 | impl Default for SshVersion { 18 | fn default() -> Self { 19 | Self { 20 | client_ver: CLIENT_VERSION.to_owned(), 21 | server_ver: String::new(), 22 | } 23 | } 24 | } 25 | 26 | /// 27 | 28 | // When the connection has been established, both sides MUST send an 29 | // identification string. This identification string MUST be 30 | 31 | // SSH-protoversion-softwareversion SP comments CR LF 32 | 33 | // Since the protocol being defined in this set of documents is version 34 | // 2.0, the 'protoversion' MUST be "2.0". The 'comments' string is 35 | // OPTIONAL. If the 'comments' string is included, a 'space' character 36 | // (denoted above as SP, ASCII 32) MUST separate the 'softwareversion' 37 | // and 'comments' strings. The identification MUST be terminated by a 38 | // single Carriage Return (CR) and a single Line Feed (LF) character 39 | // (ASCII 13 and 10, respectively). 40 | fn read_version(stream: &mut S, tm: Option) -> SshResult> 41 | where 42 | S: Read, 43 | { 44 | let mut ch = vec![0; 1]; 45 | const LF: u8 = 0xa; 46 | let crlf = vec![0xd, 0xa]; 47 | let mut outbuf = vec![]; 48 | let mut timeout = Timeout::new(tm); 49 | loop { 50 | match stream.read(&mut ch) { 51 | Ok(i) => { 52 | if 0 == i { 53 | // eof got, return 54 | return Ok(outbuf); 55 | } 56 | 57 | outbuf.extend_from_slice(&ch); 58 | 59 | if LF == ch[0] && outbuf.len() > 1 && outbuf.ends_with(&crlf) { 60 | // The server MAY send other lines of data before sending the version 61 | // string. Each line SHOULD be terminated by a Carriage Return and Line 62 | // Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded 63 | // in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients 64 | // MUST be able to process such lines. Such lines MAY be silently 65 | // ignored, or MAY be displayed to the client user. 66 | if outbuf.len() < 4 || &outbuf[0..4] != SSH_MAGIC { 67 | // skip other lines 68 | // and start read for another line 69 | outbuf.clear(); 70 | continue; 71 | } 72 | return Ok(outbuf); 73 | } 74 | timeout.renew(); 75 | } 76 | Err(e) => { 77 | if let std::io::ErrorKind::WouldBlock = e.kind() { 78 | timeout.till_next_tick()?; 79 | continue; 80 | } else { 81 | return Err(e.into()); 82 | } 83 | } 84 | }; 85 | } 86 | } 87 | 88 | impl SshVersion { 89 | pub fn read_server_version( 90 | &mut self, 91 | stream: &mut S, 92 | timeout: Option, 93 | ) -> SshResult<()> 94 | where 95 | S: Read, 96 | { 97 | let buf = read_version(stream, timeout)?; 98 | if buf.len() < 4 || &buf[0..4] != SSH_MAGIC { 99 | error!("SSH version magic doesn't match"); 100 | error!("Probably not an ssh server"); 101 | } 102 | let from_utf8 = String::from_utf8(buf)?; 103 | let version_str = from_utf8.trim(); 104 | info!("server version: [{}]", version_str); 105 | 106 | self.server_ver = version_str.to_owned(); 107 | Ok(()) 108 | } 109 | 110 | pub fn send_our_version(&self, stream: &mut S) -> SshResult<()> 111 | where 112 | S: Write, 113 | { 114 | info!("client version: [{}]", self.client_ver); 115 | let ver_string = format!("{}\r\n", self.client_ver); 116 | let _ = stream.write(ver_string.as_bytes())?; 117 | Ok(()) 118 | } 119 | 120 | pub fn validate(&self) -> SshResult<()> { 121 | if self.server_ver.contains("SSH-2.0") { 122 | Ok(()) 123 | } else { 124 | error!("error in version negotiation, version mismatch."); 125 | Err(SshError::VersionDismatchError { 126 | our: constant::CLIENT_VERSION.to_owned(), 127 | their: self.server_ver.clone(), 128 | }) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/constant.rs: -------------------------------------------------------------------------------- 1 | /// The client version 2 | pub(crate) const CLIENT_VERSION: &str = "SSH-2.0-SSH_RS-0.5.0"; 3 | pub(crate) const SSH_MAGIC: &[u8] = b"SSH-"; 4 | 5 | /// The constant strings that used for ssh communication 6 | #[allow(dead_code)] 7 | pub(crate) mod ssh_str { 8 | /// Pre-auth msg 9 | pub const SSH_USERAUTH: &str = "ssh-userauth"; 10 | /// Authenticate msg 11 | pub const SSH_CONNECTION: &str = "ssh-connection"; 12 | /// Authenticate with public key 13 | pub const PUBLIC_KEY: &str = "publickey"; 14 | /// Authenticate with password 15 | pub const PASSWORD: &str = "password"; 16 | /// Session level msg 17 | pub const SESSION: &str = "session"; 18 | /// Open a Shell 19 | pub const SHELL: &str = "shell"; 20 | /// Execute a command 21 | pub const EXEC: &str = "exec"; 22 | /// SCP 23 | pub const SCP: &str = "scp"; 24 | /// Request a pesudo-terminal 25 | pub const PTY_REQ: &str = "pty-req"; 26 | /// The xterm style that used for the pty 27 | pub const XTERM_VAR: &str = "xterm-256color"; 28 | } 29 | 30 | #[allow(dead_code)] 31 | pub(crate) mod permission { 32 | /// The default permission for directories 33 | pub const DIR: &str = "775"; 34 | /// The default permission for files 35 | pub const FILE: &str = "664"; 36 | } 37 | 38 | /// Some constants that used when scp 39 | #[cfg(feature = "scp")] 40 | #[allow(dead_code)] 41 | pub(crate) mod scp { 42 | /// Scp from our to the remote 43 | pub const SOURCE: &str = "-f"; 44 | /// Scp from the remote to our 45 | pub const SINK: &str = "-t"; 46 | /// Recursive scp for a dir 47 | pub const RECURSIVE: &str = "-r"; 48 | /// Show details 49 | pub const VERBOSE: &str = "-v"; 50 | /// Keep the modification, access time and permission the same with the origin 51 | pub const PRESERVE_TIMES: &str = "-p"; 52 | /// Show not progress bar 53 | pub const QUIET: &str = "-q"; 54 | /// Limit the bandwidth usage 55 | pub const LIMIT: &str = "-l"; 56 | 57 | /// Indicate the modification, access time of the file we recieve 58 | /// "T1647767946 0 1647767946 0\n"; 59 | pub const T: u8 = b'T'; 60 | /// Indicate that we are recieving a directory 61 | /// "D0775 0 dirName\n" 62 | pub const D: u8 = b'D'; 63 | /// Indicate that we are recieving a file 64 | /// "C0664 200 fileName.js\n" 65 | pub const C: u8 = b'C'; 66 | /// Indicate that current directory is done 67 | /// "D\n" 68 | pub const E: u8 = b'E'; 69 | 70 | /// The end flag of current operation 71 | // '\0' 72 | pub const END: u8 = 0; 73 | /// Exceptions occur 74 | pub const ERR: u8 = 1; 75 | /// Exceptions that cannot recover 76 | pub const FATAL_ERR: u8 = 2; 77 | } 78 | 79 | #[allow(dead_code)] 80 | pub(crate) mod size { 81 | pub const FILE_CHUNK: usize = 30000; 82 | /// The max size of one packet 83 | pub const BUF_SIZE: usize = 32768; 84 | /// The default window size of the flow-control 85 | pub const LOCAL_WINDOW_SIZE: u32 = 2097152; 86 | } 87 | 88 | /// 89 | #[allow(dead_code)] 90 | pub(crate) mod ssh_connection_code { 91 | pub const GLOBAL_REQUEST: u8 = 80; 92 | pub const REQUEST_SUCCESS: u8 = 81; 93 | pub const REQUEST_FAILURE: u8 = 82; 94 | pub const CHANNEL_OPEN: u8 = 90; 95 | pub const CHANNEL_OPEN_CONFIRMATION: u8 = 91; 96 | pub const CHANNEL_OPEN_FAILURE: u8 = 92; 97 | pub const CHANNEL_WINDOW_ADJUST: u8 = 93; 98 | pub const CHANNEL_DATA: u8 = 94; 99 | pub const CHANNEL_EXTENDED_DATA: u8 = 95; 100 | pub const CHANNEL_EOF: u8 = 96; 101 | pub const CHANNEL_CLOSE: u8 = 97; 102 | pub const CHANNEL_REQUEST: u8 = 98; 103 | pub const CHANNEL_SUCCESS: u8 = 99; 104 | pub const CHANNEL_FAILURE: u8 = 100; 105 | } 106 | 107 | /// 108 | #[allow(dead_code)] 109 | pub(crate) mod ssh_channel_fail_code { 110 | pub const ADMINISTRATIVELY_PROHIBITED: u32 = 1; 111 | pub const CONNECT_FAILED: u32 = 2; 112 | pub const UNKNOWN_CHANNEL_TYPE: u32 = 3; 113 | pub const RESOURCE_SHORTAGE: u32 = 4; 114 | } 115 | 116 | /// 117 | #[allow(dead_code)] 118 | pub(crate) mod ssh_transport_code { 119 | pub const DISCONNECT: u8 = 1; 120 | pub const IGNORE: u8 = 2; 121 | pub const UNIMPLEMENTED: u8 = 3; 122 | pub const DEBUG: u8 = 4; 123 | pub const SERVICE_REQUEST: u8 = 5; 124 | pub const SERVICE_ACCEPT: u8 = 6; 125 | pub const KEXINIT: u8 = 20; 126 | pub const NEWKEYS: u8 = 21; 127 | pub const KEXDH_INIT: u8 = 30; 128 | pub const KEXDH_REPLY: u8 = 31; 129 | } 130 | 131 | /// 132 | #[allow(dead_code)] 133 | pub(crate) mod ssh_disconnection_code { 134 | pub const HOST_NOT_ALLOWED_TO_CONNECT: u8 = 1; 135 | pub const PROTOCOL_ERROR: u8 = 2; 136 | pub const KEY_EXCHANGE_FAILED: u8 = 3; 137 | pub const RESERVED: u8 = 4; 138 | pub const MAC_ERROR: u8 = 5; 139 | pub const COMPRESSION_ERROR: u8 = 6; 140 | pub const SERVICE_NOT_AVAILABLE: u8 = 7; 141 | pub const PROTOCOL_VERSION_NOT_SUPPORTED: u8 = 8; 142 | pub const HOST_KEY_NOT_VERIFIABLE: u8 = 9; 143 | pub const CONNECTION_LOST: u8 = 10; 144 | pub const BY_APPLICATION: u8 = 11; 145 | pub const TOO_MANY_CONNECTIONS: u8 = 12; 146 | pub const AUTH_CANCELLED_BY_USER: u8 = 13; 147 | pub const NO_MORE_AUTH_METHODS_AVAILABLE: u8 = 14; 148 | pub const ILLEGAL_USER_NAME: u8 = 15; 149 | } 150 | 151 | /// 152 | #[allow(dead_code)] 153 | pub(crate) mod ssh_user_auth_code { 154 | pub const REQUEST: u8 = 50; 155 | pub const FAILURE: u8 = 51; 156 | pub const SUCCESS: u8 = 52; 157 | pub const BANNER: u8 = 53; 158 | pub const PK_OK: u8 = 60; 159 | } 160 | 161 | /// The magic that used when doing hash after kex 162 | pub(crate) const ALPHABET: [u8; 6] = [b'A', b'B', b'C', b'D', b'E', b'F']; 163 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{RecvError, SendError}; 2 | 3 | use thiserror::Error; 4 | 5 | pub type SshResult = Result; 6 | 7 | #[non_exhaustive] 8 | #[derive(Debug, Error)] 9 | pub enum SshError { 10 | #[error("Version dismatch: {our} vs {their}")] 11 | VersionDismatchError { our: String, their: String }, 12 | #[error("Key exchange error: {0}")] 13 | KexError(String), 14 | #[error("Parse ssh key error: {0}")] 15 | SshPubKeyError(String), 16 | #[error("Auth error")] 17 | AuthError, 18 | #[error("Timeout")] 19 | TimeoutError, 20 | #[error(transparent)] 21 | DataFormatError(#[from] std::string::FromUtf8Error), 22 | #[error("Encryption error: {0}")] 23 | EncryptionError(String), 24 | #[error("Compression error: {0}")] 25 | CompressionError(String), 26 | #[cfg(feature = "scp")] 27 | #[error(transparent)] 28 | SystemTimeError(#[from] std::time::SystemTimeError), 29 | #[cfg(feature = "scp")] 30 | #[error(transparent)] 31 | ParseIntError(#[from] std::num::ParseIntError), 32 | #[cfg(feature = "scp")] 33 | #[error("Invalid scp file path")] 34 | InvalidScpFilePath, 35 | #[cfg(feature = "scp")] 36 | #[error("Scp error: {0}")] 37 | ScpError(String), 38 | #[error(transparent)] 39 | IoError(#[from] std::io::Error), 40 | #[error("IPC error: {0}")] 41 | IpcError(String), 42 | #[error("Ssh Error: {0}")] 43 | GeneralError(String), 44 | } 45 | 46 | impl From for SshError { 47 | fn from(value: RecvError) -> Self { 48 | Self::IpcError(value.to_string()) 49 | } 50 | } 51 | 52 | impl From> for SshError { 53 | fn from(value: SendError) -> Self { 54 | Self::IpcError(value.to_string()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Dependencies 2 | //! ```toml 3 | //! ssh-rs = "0.5.0" 4 | //! ``` 5 | //! 6 | //!Rust implementation of ssh2.0 client. 7 | //! 8 | //! Basic usage 9 | //! ```no_run 10 | //! use ssh; 11 | //! 12 | //! let mut session = ssh::create_session() 13 | //! .username("ubuntu") 14 | //! .password("password") 15 | //! .private_key_path("./id_rsa") 16 | //! .connect("127.0.0.1:22") 17 | //! .unwrap() 18 | //! .run_local(); 19 | //! let exec = session.open_exec().unwrap(); 20 | //! let vec: Vec = exec.send_command("ls -all").unwrap(); 21 | //! println!("{}", String::from_utf8(vec).unwrap()); 22 | //! // Close session. 23 | //! session.close(); 24 | //! ``` 25 | //! For more usage examples and details, please see the 26 | //! [Readme](https://github.com/1148118271/ssh-rs) & 27 | //! [Examples](https://github.com/1148118271/ssh-rs/tree/main/examples) 28 | //! in our [git repo](https://github.com/1148118271/ssh-rs) 29 | //! 30 | 31 | pub mod algorithm; 32 | mod channel; 33 | mod client; 34 | mod config; 35 | mod constant; 36 | mod model; 37 | mod session; 38 | mod util; 39 | 40 | pub mod error; 41 | 42 | pub use channel::*; 43 | pub use error::SshError; 44 | pub use error::SshResult; 45 | pub use model::{TerminalSize, TerminalSizeType}; 46 | pub use session::{LocalSession, SessionBroker, SessionBuilder, SessionConnector}; 47 | 48 | /// create a session via session builder w/ default configuration 49 | /// 50 | pub fn create_session() -> SessionBuilder { 51 | SessionBuilder::new() 52 | } 53 | 54 | /// create a session via session builder w/o default configuration 55 | /// 56 | pub fn create_session_without_default() -> SessionBuilder { 57 | SessionBuilder::disable_default() 58 | } 59 | -------------------------------------------------------------------------------- /src/model/backend_msg.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use super::Data; 4 | 5 | pub(crate) enum BackendRqst { 6 | OpenChannel(u32, Data, Sender), 7 | Data(u32, Data), 8 | Command(u32, Data), 9 | CloseChannel(u32, Data), 10 | } 11 | 12 | pub(crate) enum BackendResp { 13 | Ok(u32), 14 | Fail(String), 15 | Data(Data), 16 | ExitStatus(u32), 17 | TermMsg(String), 18 | Close, 19 | } 20 | -------------------------------------------------------------------------------- /src/model/data.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use crate::error::SshResult; 4 | 5 | use super::Packet; 6 | 7 | /// Data Type Representations Used in the SSH Protocols 8 | /// 9 | 10 | /// byte 11 | /// 12 | /// A byte represents an arbitrary 8-bit value (octet). Fixed length 13 | /// data is sometimes represented as an array of bytes, written 14 | /// byte[n], where n is the number of bytes in the array. 15 | /// 16 | /// **boolean** 17 | /// 18 | /// A boolean value is stored as a single byte. The value 0 19 | /// represents FALSE, and the value 1 represents TRUE. All non-zero 20 | /// values MUST be interpreted as TRUE; however, applications MUST NOT 21 | /// store values other than 0 and 1. 22 | /// 23 | /// **uint32** 24 | /// 25 | /// Represents a 32-bit unsigned integer. Stored as four bytes in the 26 | /// order of decreasing significance (network byte order). For 27 | /// example: the value 699921578 (0x29b7f4aa) is stored as 29 b7 f4 28 | /// aa. 29 | /// 30 | /// **uint64** 31 | /// 32 | /// Represents a 64-bit unsigned integer. Stored as eight bytes in 33 | /// the order of decreasing significance (network byte order). 34 | /// 35 | /// **string** 36 | /// 37 | /// Arbitrary length binary string. Strings are allowed to contain 38 | /// arbitrary binary data, including null characters and 8-bit 39 | /// characters. They are stored as a uint32 containing its length 40 | /// (number of bytes that follow) and zero (= empty string) or more 41 | /// bytes that are the value of the string. Terminating null 42 | /// characters are not used. 43 | /// 44 | /// Strings are also used to store text. In that case, US-ASCII is 45 | /// used for internal names, and ISO-10646 UTF-8 for text that might 46 | /// be displayed to the user. The terminating null character SHOULD 47 | /// NOT normally be stored in the string. For example: the US-ASCII 48 | /// string "testing" is represented as 00 00 00 07 t e s t i n g. The 49 | /// UTF-8 mapping does not alter the encoding of US-ASCII characters. 50 | /// 51 | /// **mpint** 52 | /// 53 | /// Represents multiple precision integers in two's complement format, 54 | /// stored as a string, 8 bits per byte, MSB first. Negative numbers 55 | /// have the value 1 as the most significant bit of the first byte of 56 | /// the data partition. If the most significant bit would be set for 57 | /// a positive number, the number MUST be preceded by a zero byte. 58 | /// Unnecessary leading bytes with the value 0 or 255 MUST NOT be 59 | /// included. The value zero MUST be stored as a string with zero 60 | /// bytes of data. 61 | /// 62 | /// By convention, a number that is used in modular computations in 63 | /// Z_n SHOULD be represented in the range 0 <= x < n. 64 | /// 65 | /// Examples: 66 | /// 67 | /// value (hex) representation (hex) 68 | /// ----------- -------------------- 69 | /// 0 00 00 00 00 70 | /// 9a378f9b2e332a7 00 00 00 08 09 a3 78 f9 b2 e3 32 a7 71 | /// 80 00 00 00 02 00 80 72 | /// -1234 00 00 00 02 ed cc 73 | /// -deadbeef 00 00 00 05 ff 21 52 41 11 74 | /// 75 | /// **name-list** 76 | /// 77 | /// A string containing a comma-separated list of names. A name-list 78 | /// is represented as a uint32 containing its length (number of bytes 79 | /// that follow) followed by a comma-separated list of zero or more 80 | /// names. A name MUST have a non-zero length, and it MUST NOT 81 | /// contain a comma (","). As this is a list of names, all of the 82 | /// elements contained are names and MUST be in US-ASCII. Context may 83 | /// impose additional restrictions on the names. For example, the 84 | /// names in a name-list may have to be a list of valid algorithm 85 | /// identifiers (see Section 6 below), or a list of [RFC3066] language 86 | /// tags. The order of the names in a name-list may or may not be 87 | /// significant. Again, this depends on the context in which the list 88 | /// is used. Terminating null characters MUST NOT be used, neither 89 | /// for the individual names, nor for the list as a whole. 90 | /// 91 | /// Examples: 92 | /// 93 | /// value representation (hex) 94 | /// ----- -------------------- 95 | /// (), the empty name-list 00 00 00 00 96 | /// ("zlib") 00 00 00 04 7a 6c 69 62 97 | /// ("zlib,none") 00 00 00 09 7a 6c 69 62 2c 6e 6f 6e 65 98 | 99 | #[derive(Debug, Clone)] 100 | pub(crate) struct Data(Vec); 101 | 102 | impl Default for Data { 103 | fn default() -> Self { 104 | Self::new() 105 | } 106 | } 107 | 108 | impl Data { 109 | pub fn new() -> Data { 110 | Data(Vec::new()) 111 | } 112 | 113 | #[allow(clippy::uninit_vec)] 114 | pub fn uninit_new(len: usize) -> Data { 115 | let mut v = Vec::with_capacity(len); 116 | unsafe { v.set_len(len) } 117 | Data(v) 118 | } 119 | 120 | // write uint8 121 | pub fn put_u8(&mut self, v: u8) -> &mut Self { 122 | self.0.push(v); 123 | self 124 | } 125 | 126 | // write uint32 127 | pub fn put_u32(&mut self, v: u32) -> &mut Self { 128 | let vec = v.to_be_bytes().to_vec(); 129 | self.0.extend(&vec); 130 | self 131 | } 132 | 133 | // write string 134 | pub fn put_str(&mut self, str: &str) -> &mut Self { 135 | let v = str.as_bytes(); 136 | self.put_u32(v.len() as u32); 137 | self.0.extend(v); 138 | self 139 | } 140 | 141 | // write [bytes] 142 | pub fn put_u8s(&mut self, v: &[u8]) -> &mut Self { 143 | self.put_u32(v.len() as u32); 144 | self.0.extend(v); 145 | self 146 | } 147 | 148 | // write mpint 149 | pub fn put_mpint(&mut self, v: &[u8]) -> Vec { 150 | let mut result: Vec = Vec::new(); 151 | // 0x80 = 128 152 | if v[0] & 0x80 != 0 { 153 | result.push(0); 154 | } 155 | result.extend(v); 156 | self.put_u8s(&result).to_vec() 157 | } 158 | 159 | // skip `size` 160 | pub fn skip(&mut self, size: usize) { 161 | self.0.drain(..size); 162 | } 163 | 164 | // get uint8 165 | pub fn get_u8(&mut self) -> u8 { 166 | self.0.remove(0) 167 | } 168 | 169 | // get uint32 170 | pub fn get_u32(&mut self) -> u32 { 171 | let u32_buf = self.0.drain(..4).collect::>(); 172 | u32::from_be_bytes(u32_buf.try_into().unwrap()) 173 | } 174 | 175 | // get [bytes] 176 | pub fn get_u8s(&mut self) -> Vec { 177 | let len = self.get_u32() as usize; 178 | let bytes = self.0.drain(..len).collect::>(); 179 | bytes 180 | } 181 | 182 | pub fn into_inner(self) -> Vec { 183 | self.0 184 | } 185 | } 186 | 187 | impl From> for Data { 188 | fn from(v: Vec) -> Self { 189 | Data(v) 190 | } 191 | } 192 | 193 | impl From<&[u8]> for Data { 194 | fn from(v: &[u8]) -> Self { 195 | Data(v.into()) 196 | } 197 | } 198 | 199 | impl From for Vec { 200 | fn from(data: Data) -> Self { 201 | data.0 202 | } 203 | } 204 | 205 | impl Deref for Data { 206 | type Target = Vec; 207 | 208 | fn deref(&self) -> &Self::Target { 209 | &self.0 210 | } 211 | } 212 | 213 | impl DerefMut for Data { 214 | fn deref_mut(&mut self) -> &mut Self::Target { 215 | &mut self.0 216 | } 217 | } 218 | 219 | impl<'a> Packet<'a> for Data { 220 | fn pack(self, client: &'a mut crate::client::Client) -> super::packet::SecPacket<'a> { 221 | (self, client).into() 222 | } 223 | fn unpack(pkt: super::packet::SecPacket) -> SshResult 224 | where 225 | Self: Sized, 226 | { 227 | Ok(pkt.into_inner()) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/model/flow_control.rs: -------------------------------------------------------------------------------- 1 | use crate::constant::size::LOCAL_WINDOW_SIZE; 2 | 3 | use crate::constant::size; 4 | 5 | pub(crate) struct FlowControl { 6 | local_window: u32, 7 | remote_window: u32, 8 | } 9 | 10 | impl FlowControl { 11 | pub fn new(remote: u32) -> Self { 12 | FlowControl { 13 | local_window: LOCAL_WINDOW_SIZE, 14 | remote_window: remote, 15 | } 16 | } 17 | 18 | pub fn tune_on_recv(&mut self, buf: &mut Vec) { 19 | let recv_len = buf.len() as u32; 20 | 21 | if self.local_window >= recv_len { 22 | self.local_window -= recv_len; 23 | } else { 24 | let drop_len = recv_len - self.local_window; 25 | tracing::debug!("Recv more than expected, drop len {}", drop_len); 26 | buf.truncate(self.local_window as usize); 27 | self.local_window = 0; 28 | } 29 | } 30 | 31 | pub fn tune_on_send(&mut self, buf: &mut Vec) -> Vec { 32 | let want_send = buf.len(); 33 | 34 | let can_send = { 35 | let mut can_send = want_send; 36 | 37 | if can_send > self.remote_window as usize { 38 | can_send = self.remote_window as usize 39 | } 40 | 41 | if can_send > size::BUF_SIZE { 42 | can_send = size::BUF_SIZE 43 | } 44 | can_send 45 | }; 46 | 47 | self.remote_window -= can_send as u32; 48 | 49 | buf.split_off(can_send) 50 | } 51 | 52 | pub fn on_recv(&mut self, size: u32) { 53 | self.remote_window += size 54 | } 55 | 56 | pub fn on_send(&mut self, size: u32) { 57 | self.local_window += size 58 | } 59 | 60 | pub fn can_send(&self) -> bool { 61 | self.remote_window > 0 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | mod backend_msg; 2 | mod data; 3 | mod flow_control; 4 | mod packet; 5 | mod sequence; 6 | mod terminal; 7 | mod timeout; 8 | mod u32iter; 9 | 10 | #[cfg(feature = "scp")] 11 | mod scp_file; 12 | 13 | use std::{ 14 | cell::RefCell, 15 | rc::Rc, 16 | sync::{Arc, Mutex}, 17 | }; 18 | 19 | pub use terminal::*; 20 | 21 | pub(crate) use backend_msg::*; 22 | pub(crate) use data::Data; 23 | pub(crate) use flow_control::FlowControl; 24 | pub(crate) use packet::{Packet, SecPacket}; 25 | pub(crate) use sequence::Sequence; 26 | pub(crate) use timeout::Timeout; 27 | pub(crate) use u32iter::U32Iter; 28 | 29 | #[cfg(feature = "scp")] 30 | pub(crate) use scp_file::ScpFile; 31 | 32 | pub(crate) type RcMut = Rc>; 33 | pub(crate) type ArcMut = Arc>; 34 | -------------------------------------------------------------------------------- /src/model/packet.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::time::Duration; 3 | 4 | use crate::error::SshResult; 5 | use crate::{client::Client, model::Data}; 6 | 7 | use super::timeout::Timeout; 8 | 9 | /// ## Binary Packet Protocol 10 | /// 11 | /// 12 | /// 13 | /// uint32 `packet_length` 14 | /// 15 | /// byte `padding_length` 16 | /// 17 | /// byte[[n1]] `payload`; n1 = packet_length - padding_length - 1 18 | /// 19 | /// byte[[n2]] `random padding`; n2 = padding_length 20 | /// 21 | /// byte[[m]] `mac` (Message Authentication Code - MAC); m = mac_length 22 | /// 23 | /// --- 24 | /// 25 | /// **packet_length** 26 | /// The length of the packet in bytes, not including 'mac' or the 'packet_length' field itself. 27 | /// 28 | /// 29 | /// **padding_length** 30 | /// Length of 'random padding' (bytes). 31 | /// 32 | /// 33 | /// **payload** 34 | /// The useful contents of the packet. If compression has been negotiated, this field is compressed. 35 | /// Initially, compression MUST be "none". 36 | /// 37 | /// 38 | /// **random padding** 39 | /// Arbitrary-length padding, such that the total length of 40 | /// (packet_length || padding_length || payload || random padding) 41 | /// is a multiple of the cipher block size or 8, whichever is 42 | /// larger. There MUST be at least four bytes of padding. The 43 | /// padding SHOULD consist of random bytes. The maximum amount of 44 | /// padding is 255 bytes. 45 | 46 | /// 47 | /// **mac** 48 | /// Message Authentication Code. If message authentication has 49 | /// been negotiated, this field contains the MAC bytes. Initially, 50 | /// the MAC algorithm MUST be "none".。 51 | 52 | fn read_with_timeout(stream: &mut S, tm: Option, buf: &mut [u8]) -> SshResult<()> 53 | where 54 | S: Read, 55 | { 56 | let want_len = buf.len(); 57 | let mut offset = 0; 58 | let mut timeout = Timeout::new(tm); 59 | 60 | loop { 61 | match stream.read(&mut buf[offset..]) { 62 | Ok(i) => { 63 | offset += i; 64 | if offset == want_len { 65 | return Ok(()); 66 | } else { 67 | timeout.renew(); 68 | continue; 69 | } 70 | } 71 | Err(e) => { 72 | if let std::io::ErrorKind::WouldBlock = e.kind() { 73 | timeout.till_next_tick()?; 74 | continue; 75 | } else { 76 | return Err(e.into()); 77 | } 78 | } 79 | }; 80 | } 81 | } 82 | 83 | fn try_read(stream: &mut S, _tm: Option, buf: &mut [u8]) -> SshResult 84 | where 85 | S: Read, 86 | { 87 | match stream.read(buf) { 88 | Ok(i) => Ok(i), 89 | Err(e) => { 90 | if let std::io::ErrorKind::WouldBlock = e.kind() { 91 | Ok(0) 92 | } else { 93 | Err(e.into()) 94 | } 95 | } 96 | } 97 | } 98 | 99 | fn write_with_timeout(stream: &mut S, tm: Option, buf: &[u8]) -> SshResult<()> 100 | where 101 | S: Write, 102 | { 103 | let want_len = buf.len(); 104 | let mut offset = 0; 105 | let mut timeout = Timeout::new(tm); 106 | 107 | loop { 108 | match stream.write(&buf[offset..]) { 109 | Ok(i) => { 110 | offset += i; 111 | if offset == want_len { 112 | return Ok(()); 113 | } else { 114 | timeout.renew(); 115 | continue; 116 | } 117 | } 118 | Err(e) => { 119 | if let std::io::ErrorKind::WouldBlock = e.kind() { 120 | timeout.till_next_tick()?; 121 | continue; 122 | } else { 123 | return Err(e.into()); 124 | } 125 | } 126 | }; 127 | } 128 | } 129 | 130 | pub(crate) trait Packet<'a> { 131 | fn pack(self, client: &'a mut Client) -> SecPacket<'a>; 132 | fn unpack(pkt: SecPacket) -> SshResult 133 | where 134 | Self: Sized; 135 | } 136 | 137 | pub(crate) struct SecPacket<'a> { 138 | payload: Data, 139 | client: &'a mut Client, 140 | } 141 | 142 | impl<'a> SecPacket<'a> { 143 | fn get_align(bsize: usize) -> i32 { 144 | let bsize = bsize as i32; 145 | if bsize > 8 { 146 | bsize 147 | } else { 148 | 8 149 | } 150 | } 151 | 152 | pub fn write_stream(self, stream: &mut S) -> SshResult<()> 153 | where 154 | S: Write, 155 | { 156 | let tm = self.client.get_timeout(); 157 | let payload = self.client.get_compressor().compress(&self.payload)?; 158 | let payload_len = payload.len() as u32; 159 | let pad_len = { 160 | let mut pad = payload_len as i32 + 1; 161 | let block_size = Self::get_align(self.client.get_encryptor().bsize()); 162 | if !self.client.get_encryptor().no_pad() { 163 | pad += 4 164 | } 165 | (((-pad) & (block_size - 1)) + block_size) as u32 166 | } as u8; 167 | let packet_len = 1 + pad_len as u32 + payload_len; 168 | let mut buf = vec![]; 169 | buf.extend(packet_len.to_be_bytes()); 170 | buf.extend([pad_len]); 171 | buf.extend(payload); 172 | buf.extend(vec![0; pad_len as usize]); 173 | let seq = self.client.get_seq().get_client(); 174 | self.client.get_encryptor().encrypt(seq, &mut buf); 175 | write_with_timeout(stream, tm, &buf) 176 | } 177 | 178 | pub fn from_stream(stream: &mut S, client: &'a mut Client) -> SshResult 179 | where 180 | S: Read, 181 | { 182 | let tm = client.get_timeout(); 183 | let bsize = Self::get_align(client.get_encryptor().bsize()) as usize; 184 | 185 | // read the first block 186 | let mut first_block = vec![0; bsize]; 187 | read_with_timeout(stream, tm, &mut first_block)?; 188 | 189 | // detect the total len 190 | let seq = client.get_seq().get_server(); 191 | let data_len = client.get_encryptor().data_len(seq, &first_block); 192 | 193 | // read remain 194 | let mut data = Data::uninit_new(data_len); 195 | data[0..bsize].clone_from_slice(&first_block); 196 | read_with_timeout(stream, tm, &mut data[bsize..])?; 197 | 198 | // decrypt all 199 | let data = client.get_encryptor().decrypt(seq, &mut data)?; 200 | 201 | // unpacking 202 | let pkt_len = u32::from_be_bytes(data[0..4].try_into().unwrap()); 203 | let pad_len = data[4]; 204 | let payload_len = pkt_len - pad_len as u32 - 1; 205 | 206 | let payload = data[5..payload_len as usize + 5].into(); 207 | let payload = client.get_compressor().decompress(payload)?.into(); 208 | 209 | Ok(Self { payload, client }) 210 | } 211 | 212 | pub fn try_from_stream(stream: &mut S, client: &'a mut Client) -> SshResult> 213 | where 214 | S: Read, 215 | { 216 | let tm = client.get_timeout(); 217 | let bsize = Self::get_align(client.get_encryptor().bsize()) as usize; 218 | 219 | // read the first block 220 | let mut first_block = vec![0; bsize]; 221 | let read = try_read(stream, tm, &mut first_block)?; 222 | if read == 0 { 223 | return Ok(None); 224 | } 225 | 226 | // detect the total len 227 | let seq = client.get_seq().get_server(); 228 | let data_len = client.get_encryptor().data_len(seq, &first_block); 229 | 230 | // read remain 231 | let mut data = Data::uninit_new(data_len); 232 | data[0..bsize].clone_from_slice(&first_block); 233 | read_with_timeout(stream, tm, &mut data[bsize..])?; 234 | 235 | // decrypt all 236 | let data = client.get_encryptor().decrypt(seq, &mut data)?; 237 | 238 | // unpacking 239 | let pkt_len = u32::from_be_bytes(data[0..4].try_into().unwrap()); 240 | let pad_len = data[4]; 241 | let payload_len = pkt_len - pad_len as u32 - 1; 242 | 243 | let payload = data[5..payload_len as usize + 5].into(); 244 | 245 | Ok(Some(Self { payload, client })) 246 | } 247 | 248 | pub fn get_inner(&self) -> &[u8] { 249 | &self.payload 250 | } 251 | 252 | pub fn into_inner(self) -> Data { 253 | self.payload 254 | } 255 | } 256 | 257 | impl<'a> From<(Data, &'a mut Client)> for SecPacket<'a> { 258 | fn from((d, c): (Data, &'a mut Client)) -> Self { 259 | Self { 260 | payload: d, 261 | client: c, 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/model/scp_file.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub(crate) struct ScpFile { 4 | pub modify_time: i64, 5 | pub access_time: i64, 6 | pub size: u64, 7 | pub name: String, 8 | pub is_dir: bool, 9 | pub local_path: PathBuf, 10 | } 11 | 12 | impl ScpFile { 13 | pub fn new() -> Self { 14 | ScpFile { 15 | modify_time: 0, 16 | access_time: 0, 17 | size: 0, 18 | name: String::new(), 19 | is_dir: false, 20 | local_path: Default::default(), 21 | } 22 | } 23 | 24 | pub fn join(&self, filename: &str) -> PathBuf { 25 | if self.local_path.is_dir() { 26 | self.local_path.join(filename) 27 | } else { 28 | self.local_path.clone() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/model/sequence.rs: -------------------------------------------------------------------------------- 1 | use super::U32Iter; 2 | 3 | #[derive(Default)] 4 | pub(crate) struct Sequence { 5 | client_sequence_num: U32Iter, 6 | server_sequence_num: U32Iter, 7 | } 8 | 9 | impl Sequence { 10 | pub fn get_client(&mut self) -> u32 { 11 | self.client_sequence_num.next().unwrap() 12 | } 13 | 14 | pub fn get_server(&mut self) -> u32 { 15 | self.server_sequence_num.next().unwrap() 16 | } 17 | 18 | pub fn new() -> Self { 19 | Self { 20 | ..Default::default() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/model/terminal.rs: -------------------------------------------------------------------------------- 1 | pub enum TerminalSizeType { 2 | Character, 3 | Pixel, 4 | } 5 | 6 | pub struct TerminalSize { 7 | width: u32, 8 | height: u32, 9 | type_: TerminalSizeType, 10 | } 11 | 12 | impl TerminalSize { 13 | pub fn from(width: u32, height: u32) -> Self { 14 | TerminalSize { 15 | width, 16 | height, 17 | type_: TerminalSizeType::Character, 18 | } 19 | } 20 | 21 | pub fn from_type(width: u32, height: u32, type_: TerminalSizeType) -> Self { 22 | TerminalSize { 23 | width, 24 | height, 25 | type_, 26 | } 27 | } 28 | 29 | pub(crate) fn fetch(&self) -> (u32, u32, u32, u32) { 30 | match self.type_ { 31 | TerminalSizeType::Character => (self.width, self.height, 0, 0), 32 | TerminalSizeType::Pixel => (0, 0, self.width, self.height), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/model/timeout.rs: -------------------------------------------------------------------------------- 1 | use crate::{SshError, SshResult}; 2 | use std::time::{Duration, Instant}; 3 | 4 | #[cfg(not(target_arch = "wasm32"))] 5 | const NANOS_PER_SEC: u64 = 1_000_000_000; 6 | 7 | pub(crate) struct Timeout { 8 | instant: Instant, 9 | timeout: Option, 10 | wait_tick: u64, 11 | } 12 | 13 | impl Timeout { 14 | pub fn new(timeout: Option) -> Self { 15 | Timeout { 16 | instant: Instant::now(), 17 | timeout, 18 | wait_tick: 1, 19 | } 20 | } 21 | 22 | fn wait(&mut self) -> u64 { 23 | #[cfg(not(target_arch = "wasm32"))] 24 | { 25 | let sleep_time = Duration::from_nanos(self.wait_tick); 26 | std::thread::sleep(sleep_time); 27 | if self.wait_tick < NANOS_PER_SEC { 28 | self.wait_tick <<= 1; 29 | } 30 | 31 | if let Some(timemout) = self.timeout { 32 | let timeout_nanos = timemout.as_nanos(); 33 | let used_nanos = self.instant.elapsed().as_nanos(); 34 | 35 | self.wait_tick = { 36 | if timeout_nanos > used_nanos 37 | && timeout_nanos - used_nanos < self.wait_tick as u128 38 | { 39 | (timeout_nanos - used_nanos) as u64 40 | } else { 41 | self.wait_tick 42 | } 43 | }; 44 | } 45 | } 46 | self.wait_tick 47 | } 48 | 49 | pub fn till_next_tick(&mut self) -> SshResult<()> { 50 | if let Some(t) = self.timeout { 51 | if self.instant.elapsed() > t { 52 | tracing::error!("time out."); 53 | Err(SshError::TimeoutError) 54 | } else { 55 | self.wait(); 56 | Ok(()) 57 | } 58 | } else { 59 | self.wait(); 60 | Ok(()) 61 | } 62 | } 63 | 64 | pub fn renew(&mut self) { 65 | self.wait_tick = 1 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/model/u32iter.rs: -------------------------------------------------------------------------------- 1 | pub(crate) struct U32Iter { 2 | num: u32, 3 | } 4 | 5 | impl Iterator for U32Iter { 6 | type Item = u32; 7 | 8 | fn next(&mut self) -> Option { 9 | if self.num == u32::MAX { 10 | self.num = 0; 11 | } else { 12 | self.num += 1; 13 | } 14 | Some(self.num) 15 | } 16 | } 17 | 18 | impl Default for U32Iter { 19 | fn default() -> Self { 20 | Self { num: u32::MAX } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/session/session_local.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::RefCell, 3 | io::{Read, Write}, 4 | rc::Rc, 5 | time::Duration, 6 | }; 7 | use tracing::*; 8 | 9 | #[cfg(feature = "scp")] 10 | use crate::channel::LocalScp; 11 | use crate::{ 12 | channel::{LocalChannel, LocalExec, LocalShell}, 13 | client::Client, 14 | constant::{size, ssh_channel_fail_code, ssh_connection_code, ssh_str}, 15 | error::{SshError, SshResult}, 16 | model::TerminalSize, 17 | model::{Data, Packet, RcMut, SecPacket, U32Iter}, 18 | }; 19 | 20 | pub struct LocalSession 21 | where 22 | S: Read + Write, 23 | { 24 | client: RcMut, 25 | stream: RcMut, 26 | channel_num: U32Iter, 27 | } 28 | 29 | impl LocalSession 30 | where 31 | S: Read + Write, 32 | { 33 | pub(crate) fn new(client: Client, stream: S) -> Self { 34 | Self { 35 | client: Rc::new(RefCell::new(client)), 36 | stream: Rc::new(RefCell::new(stream)), 37 | channel_num: U32Iter::default(), 38 | } 39 | } 40 | 41 | /// close the local session and consume it 42 | /// 43 | pub fn close(self) { 44 | info!("Client close"); 45 | drop(self) 46 | } 47 | 48 | /// Modify the timeout setting 49 | /// in case the user wants to change the timeout during ssh operations. 50 | /// 51 | pub fn set_timeout(&mut self, timeout: Option) { 52 | self.client.borrow_mut().set_timeout(timeout) 53 | } 54 | 55 | /// open a [LocalExec] channel which can excute commands 56 | /// 57 | pub fn open_exec(&mut self) -> SshResult> { 58 | let channel = self.open_channel()?; 59 | channel.exec() 60 | } 61 | 62 | /// open a [LocalScp] channel which can download/upload files/directories 63 | /// 64 | #[cfg(feature = "scp")] 65 | pub fn open_scp(&mut self) -> SshResult> { 66 | let channel = self.open_channel()?; 67 | channel.scp() 68 | } 69 | 70 | /// open a [LocalShell] channel which can download/upload files/directories 71 | /// 72 | pub fn open_shell(&mut self) -> SshResult> { 73 | self.open_shell_terminal(TerminalSize::from(80, 24)) 74 | } 75 | 76 | /// open a [LocalShell] channel 77 | /// 78 | /// custom terminal dimensions 79 | /// 80 | pub fn open_shell_terminal(&mut self, tv: TerminalSize) -> SshResult> { 81 | let channel = self.open_channel()?; 82 | channel.shell(tv) 83 | } 84 | 85 | pub fn get_raw_io(&mut self) -> RcMut { 86 | self.stream.clone() 87 | } 88 | 89 | /// open a raw channel 90 | /// 91 | /// need call `.exec()`, `.shell()`, `.scp()` and so on to convert it to a specific channel 92 | /// 93 | pub fn open_channel(&mut self) -> SshResult> { 94 | info!("channel opened."); 95 | 96 | let client_channel_no = self.channel_num.next().unwrap(); 97 | self.send_open_channel(client_channel_no)?; 98 | let (server_channel_no, remote_window_size) = self.receive_open_channel()?; 99 | 100 | Ok(LocalChannel::new( 101 | server_channel_no, 102 | client_channel_no, 103 | remote_window_size, 104 | self.client.clone(), 105 | self.stream.clone(), 106 | )) 107 | } 108 | 109 | // open channel request 110 | fn send_open_channel(&mut self, client_channel_no: u32) -> SshResult<()> { 111 | let mut data = Data::new(); 112 | data.put_u8(ssh_connection_code::CHANNEL_OPEN) 113 | .put_str(ssh_str::SESSION) 114 | .put_u32(client_channel_no) 115 | .put_u32(size::LOCAL_WINDOW_SIZE) 116 | .put_u32(size::BUF_SIZE as u32); 117 | data.pack(&mut self.client.borrow_mut()) 118 | .write_stream(&mut *self.stream.borrow_mut()) 119 | } 120 | 121 | // get the response of the channel request 122 | fn receive_open_channel(&mut self) -> SshResult<(u32, u32)> { 123 | loop { 124 | let mut data = Data::unpack(SecPacket::from_stream( 125 | &mut *self.stream.borrow_mut(), 126 | &mut self.client.borrow_mut(), 127 | )?)?; 128 | 129 | let message_code = data.get_u8(); 130 | match message_code { 131 | // Successfully open a channel 132 | ssh_connection_code::CHANNEL_OPEN_CONFIRMATION => { 133 | data.get_u32(); 134 | let server_channel_no = data.get_u32(); 135 | let remote_window_size = data.get_u32(); 136 | // remote packet size, currently don't need it 137 | data.get_u32(); 138 | return Ok((server_channel_no, remote_window_size)); 139 | } 140 | /* 141 | byte CHANNEL_OPEN_FAILURE 142 | uint32 recipient channel 143 | uint32 reason code 144 | string description,ISO-10646 UTF-8 [RFC3629] 145 | string language tag,[RFC3066] 146 | */ 147 | // Fail to open a channel 148 | ssh_connection_code::CHANNEL_OPEN_FAILURE => { 149 | data.get_u32(); 150 | // error code 151 | let code = data.get_u32(); 152 | // error detail: By default is utf-8 153 | let description = 154 | String::from_utf8(data.get_u8s()).unwrap_or_else(|_| String::from("error")); 155 | // language tag, assume to be en-US 156 | data.get_u8s(); 157 | 158 | let err_msg = match code { 159 | ssh_channel_fail_code::ADMINISTRATIVELY_PROHIBITED => { 160 | format!("ADMINISTRATIVELY_PROHIBITED: {}", description) 161 | } 162 | ssh_channel_fail_code::CONNECT_FAILED => { 163 | format!("CONNECT_FAILED: {}", description) 164 | } 165 | ssh_channel_fail_code::UNKNOWN_CHANNEL_TYPE => { 166 | format!("UNKNOWN_CHANNEL_TYPE: {}", description) 167 | } 168 | ssh_channel_fail_code::RESOURCE_SHORTAGE => { 169 | format!("RESOURCE_SHORTAGE: {}", description) 170 | } 171 | _ => description, 172 | }; 173 | return Err(SshError::GeneralError(err_msg)); 174 | } 175 | ssh_connection_code::GLOBAL_REQUEST => { 176 | let mut data = Data::new(); 177 | data.put_u8(ssh_connection_code::REQUEST_FAILURE); 178 | data.pack(&mut self.client.borrow_mut()) 179 | .write_stream(&mut *self.stream.borrow_mut())?; 180 | continue; 181 | } 182 | x => { 183 | debug!("Ignore ssh msg {}", x); 184 | continue; 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::error::SshResult; 2 | use rand::rngs::OsRng; 3 | use rand::Rng; 4 | 5 | #[cfg(feature = "scp")] 6 | use crate::error::SshError; 7 | #[cfg(feature = "scp")] 8 | use std::{ 9 | path::Path, 10 | str::FromStr, 11 | time::{SystemTime, UNIX_EPOCH}, 12 | }; 13 | 14 | #[cfg(feature = "scp")] 15 | pub(crate) fn sys_time_to_secs(time: SystemTime) -> SshResult { 16 | Ok(time.duration_since(UNIX_EPOCH)?.as_secs()) 17 | } 18 | 19 | // a random cookie 20 | pub(crate) fn cookie() -> Vec { 21 | let cookie: [u8; 16] = OsRng.gen(); 22 | cookie.to_vec() 23 | } 24 | 25 | pub(crate) fn vec_u8_to_string(v: Vec, pat: &str) -> SshResult> { 26 | let result = String::from_utf8(v)?; 27 | let r: Vec<&str> = result.split(pat).collect(); 28 | let mut vec = vec![]; 29 | for x in r { 30 | vec.push(x.to_owned()) 31 | } 32 | Ok(vec) 33 | } 34 | 35 | #[cfg(feature = "scp")] 36 | pub(crate) fn check_path(path: &Path) -> SshResult<()> { 37 | if path.to_str().is_none() { 38 | return Err(SshError::InvalidScpFilePath); 39 | } 40 | Ok(()) 41 | } 42 | 43 | #[cfg(feature = "scp")] 44 | pub(crate) fn file_time(v: Vec) -> SshResult<(i64, i64)> { 45 | let mut t = vec![]; 46 | for x in v { 47 | if x == b'T' || x == 32 || x == 10 { 48 | continue; 49 | } 50 | t.push(x) 51 | } 52 | let a = t.len() / 2; 53 | let ct = String::from_utf8(t[..(a - 1)].to_vec())?; 54 | let ut = String::from_utf8(t[a..(t.len() - 1)].to_vec())?; 55 | Ok((i64::from_str(&ct)?, i64::from_str(&ut)?)) 56 | } 57 | -------------------------------------------------------------------------------- /tests/connect.rs: -------------------------------------------------------------------------------- 1 | mod tests { 2 | use paste::paste; 3 | 4 | use std::env; 5 | 6 | macro_rules! env_getter { 7 | ($field:ident, $default: expr) => { 8 | paste! { 9 | pub fn []() -> String { 10 | env::var("SSH_RS_TEST_".to_owned() + stringify!([<$field:upper>])).unwrap_or($default.to_owned()) 11 | } 12 | } 13 | }; 14 | } 15 | env_getter!(username, "ubuntu"); 16 | env_getter!(passwd, "password"); 17 | env_getter!(server, "127.0.0.1:22"); 18 | env_getter!(pem_rsa, "./rsa_old"); 19 | env_getter!(openssh_rsa, "./rsa_new"); 20 | env_getter!(ed25519, "./ed25519"); 21 | 22 | #[test] 23 | fn test_password() { 24 | let session = ssh::create_session() 25 | .username(&get_username()) 26 | .password(&get_passwd()) 27 | .connect(get_server()) 28 | .unwrap() 29 | .run_local(); 30 | session.close(); 31 | } 32 | 33 | #[test] 34 | fn test_password_backend() { 35 | let session = ssh::create_session() 36 | .username(&get_username()) 37 | .password(&get_passwd()) 38 | .connect(get_server()) 39 | .unwrap() 40 | .run_backend(); 41 | session.close(); 42 | } 43 | 44 | #[test] 45 | fn test_rsa_old() { 46 | let session = ssh::create_session() 47 | .username(&get_username()) 48 | .private_key_path(get_pem_rsa()) 49 | .connect(get_server()) 50 | .unwrap() 51 | .run_local(); 52 | session.close(); 53 | } 54 | 55 | #[test] 56 | fn test_rsa_new() { 57 | let session = ssh::create_session() 58 | .username(&get_username()) 59 | .private_key_path(get_openssh_rsa()) 60 | .connect(get_server()) 61 | .unwrap() 62 | .run_local(); 63 | session.close(); 64 | } 65 | 66 | #[test] 67 | fn test_ed25519() { 68 | let session = ssh::create_session() 69 | .username(&get_username()) 70 | .private_key_path(get_ed25519()) 71 | .connect(get_server()) 72 | .unwrap() 73 | .run_local(); 74 | session.close(); 75 | } 76 | 77 | #[test] 78 | fn test_pubkey_fallback() { 79 | let session = ssh::create_session() 80 | .username(&get_username()) 81 | .password(&get_passwd()) 82 | .private_key_path("") 83 | .connect(get_server()) 84 | .unwrap() 85 | .run_local(); 86 | session.close(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/exec.rs: -------------------------------------------------------------------------------- 1 | mod tests { 2 | use paste::paste; 3 | 4 | use std::env; 5 | 6 | macro_rules! env_getter { 7 | ($field:ident, $default: expr) => { 8 | paste! { 9 | pub fn []() -> String { 10 | env::var("SSH_RS_TEST_".to_owned() + stringify!([<$field:upper>])).unwrap_or($default.to_owned()) 11 | } 12 | } 13 | }; 14 | } 15 | env_getter!(username, "ubuntu"); 16 | env_getter!(server, "127.0.0.1:22"); 17 | env_getter!(pem_rsa, "./rsa_old"); 18 | 19 | #[test] 20 | fn test_exec() { 21 | let mut session = ssh::create_session() 22 | .username(&get_username()) 23 | .private_key_path(get_pem_rsa()) 24 | .connect(get_server()) 25 | .unwrap() 26 | .run_local(); 27 | let exec = session.open_exec().unwrap(); 28 | let vec: Vec = exec.send_command("ls -all").unwrap(); 29 | println!("{}", String::from_utf8(vec).unwrap()); 30 | // Close session. 31 | session.close(); 32 | } 33 | 34 | #[test] 35 | fn test_exec_backend() { 36 | let mut session = ssh::create_session() 37 | .username(&get_username()) 38 | .private_key_path(get_pem_rsa()) 39 | .connect(get_server()) 40 | .unwrap() 41 | .run_backend(); 42 | let mut exec = session.open_exec().unwrap(); 43 | 44 | const CMD: &str = "ls -lah"; 45 | 46 | // send the command to server 47 | println!("Send command {}", CMD); 48 | exec.send_command(CMD).unwrap(); 49 | 50 | // process other data 51 | println!("Do someother thing"); 52 | 53 | // get command result 54 | let vec: Vec = exec.get_result().unwrap(); 55 | println!("{}", String::from_utf8(vec).unwrap()); 56 | 57 | // Close session. 58 | session.close(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/scp.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "scp")] 2 | mod test { 3 | use paste::paste; 4 | use ssh::{self, LocalSession}; 5 | use std::env; 6 | 7 | fn remove_file(filename: &str) { 8 | let file = std::path::Path::new(filename); 9 | 10 | std::fs::remove_file(file).unwrap_or_else(|_| { 11 | std::process::Command::new("sudo") 12 | .arg("rm") 13 | .arg("-rf") 14 | .arg(filename) 15 | .output() 16 | .unwrap(); 17 | }); 18 | } 19 | 20 | fn assert_file_eq(file_1: &str, file_2: &str) { 21 | let md5_1 = std::process::Command::new("md5sum") 22 | .arg(file_1) 23 | .output() 24 | .expect("cannot calc the md5"); 25 | let md5_2 = std::process::Command::new("md5sum") 26 | .arg(file_2) 27 | .output() 28 | .expect("cannot calc the md5"); 29 | assert_eq!(md5_1.stdout[0..32], md5_2.stdout[0..32]); 30 | } 31 | 32 | fn remove_dir(dirname: &str) { 33 | let dir = std::path::Path::new(dirname); 34 | assert!(dir.exists()); 35 | std::fs::remove_dir(dir).unwrap_or_else(|_| { 36 | std::process::Command::new("sudo") 37 | .arg("rm") 38 | .arg("-rf") 39 | .arg(dirname) 40 | .output() 41 | .unwrap(); 42 | }); 43 | } 44 | 45 | fn create_file(path: &str, cap: &str, peer: bool) { 46 | let _ = std::process::Command::new("fallocate") 47 | .arg("-l") 48 | .arg(cap) 49 | .arg(path) 50 | .output(); 51 | let _ = std::process::Command::new("sudo") 52 | .arg("fallocate") 53 | .arg("-l") 54 | .arg(cap) 55 | .arg(path) 56 | .output(); 57 | 58 | if peer { 59 | let _ = std::process::Command::new("chown") 60 | .arg(&get_username()) 61 | .arg(path) 62 | .output(); 63 | let _ = std::process::Command::new("sudo") 64 | .arg("chown") 65 | .arg(&get_username()) 66 | .arg(path) 67 | .output(); 68 | } 69 | } 70 | 71 | fn create_dir(path: &str, peer: bool) { 72 | std::fs::create_dir(path).unwrap_or_else(|_| { 73 | std::process::Command::new("sudo") 74 | .arg("mkdir") 75 | .arg(path) 76 | .output() 77 | .unwrap(); 78 | }); 79 | 80 | if peer { 81 | let _ = std::process::Command::new("chown") 82 | .arg(&get_username()) 83 | .arg("-R") 84 | .arg(path) 85 | .output(); 86 | let _ = std::process::Command::new("sudo") 87 | .arg("chown") 88 | .arg(&get_username()) 89 | .arg("-R") 90 | .arg(path) 91 | .output(); 92 | } 93 | } 94 | 95 | macro_rules! env_getter { 96 | ($field:ident, $default: expr) => { 97 | paste! { 98 | pub fn []() -> String { 99 | env::var("SSH_RS_TEST_".to_owned() + stringify!([<$field:upper>])).unwrap_or($default.to_owned()) 100 | } 101 | } 102 | }; 103 | } 104 | env_getter!(username, "ubuntu"); 105 | env_getter!(server, "127.0.0.1:22"); 106 | env_getter!(pem_rsa, "./rsa_old"); 107 | 108 | fn get_target_path(path: &str) -> String { 109 | format!("/home/{}/{}", get_username(), path) 110 | } 111 | 112 | fn create_session() -> LocalSession { 113 | ssh::create_session() 114 | .username(&get_username()) 115 | .private_key_path(get_pem_rsa()) 116 | .connect(get_server()) 117 | .unwrap() 118 | .run_local() 119 | } 120 | #[test] 121 | fn test_upload_file() { 122 | let mut session = create_session(); 123 | 124 | create_file("test_file1", "2M", false); 125 | 126 | let scp = session.open_scp().unwrap(); 127 | scp.upload("test_file1", "./").unwrap(); 128 | assert_file_eq("test_file1", &get_target_path("test_file1")); 129 | remove_file(&get_target_path("test_file1")); 130 | remove_file("test_file1"); 131 | 132 | session.close(); 133 | } 134 | 135 | #[test] 136 | fn test_upload_file_rename() { 137 | let mut session = create_session(); 138 | 139 | create_file("test_file2", "2M", false); 140 | 141 | let scp = session.open_scp().unwrap(); 142 | scp.upload("test_file2", "./test_rename").unwrap(); 143 | assert_file_eq("test_file2", &get_target_path("test_rename")); 144 | remove_file(&get_target_path("test_rename")); 145 | remove_file("test_file2"); 146 | 147 | session.close(); 148 | } 149 | 150 | #[test] 151 | fn test_upload_to_implicit_dir() { 152 | let mut session = create_session(); 153 | 154 | create_dir(&get_target_path("test_dir1"), true); 155 | create_file("test_file3", "2M", false); 156 | 157 | let scp = session.open_scp().unwrap(); 158 | scp.upload("test_file3", "./test_dir1").unwrap(); 159 | assert_file_eq("test_file3", &get_target_path("test_dir1/test_file3")); 160 | remove_file(&get_target_path("test_dir1/test_file3")); 161 | remove_file("test_file3"); 162 | remove_dir(&get_target_path("test_dir1")); 163 | 164 | session.close(); 165 | } 166 | 167 | #[test] 168 | fn test_upload_dir() { 169 | let mut session = create_session(); 170 | 171 | create_dir("test_dir2", false); 172 | 173 | let scp = session.open_scp().unwrap(); 174 | scp.upload("test_dir2", "./").unwrap(); 175 | remove_dir(&get_target_path("test_dir2")); 176 | remove_dir("test_dir2"); 177 | 178 | session.close(); 179 | } 180 | 181 | #[test] 182 | fn test_download_file() { 183 | let mut session = create_session(); 184 | 185 | create_file(&get_target_path("test_file4"), "2M", true); 186 | 187 | let scp = session.open_scp().unwrap(); 188 | scp.download("test_file4", "test_file4").unwrap(); 189 | assert_file_eq("test_file4", &get_target_path("test_file4")); 190 | remove_file(&get_target_path("test_file4")); 191 | remove_file("test_file4"); 192 | 193 | session.close(); 194 | } 195 | 196 | #[test] 197 | fn test_download_file_rename() { 198 | let mut session = create_session(); 199 | 200 | create_file(&get_target_path("test_file5"), "2M", true); 201 | 202 | let scp = session.open_scp().unwrap(); 203 | scp.download("test_rename", "test_file5").unwrap(); 204 | assert_file_eq("test_rename", &get_target_path("test_file5")); 205 | remove_file(&get_target_path("test_file5")); 206 | remove_file("test_rename"); 207 | 208 | session.close(); 209 | } 210 | 211 | #[test] 212 | fn test_download_to_implicit_dir() { 213 | let mut session = create_session(); 214 | 215 | create_file(&get_target_path("test_file6"), "2M", true); 216 | create_dir("test_dir3", false); 217 | 218 | let scp = session.open_scp().unwrap(); 219 | scp.download("test_dir3", "test_file6").unwrap(); 220 | assert_file_eq("test_dir3/test_file6", &get_target_path("test_file6")); 221 | remove_file(&get_target_path("test_file6")); 222 | remove_file("test_dir3/test_file6"); 223 | remove_dir("test_dir3"); 224 | 225 | session.close(); 226 | } 227 | 228 | #[test] 229 | fn test_download_dir() { 230 | let mut session = create_session(); 231 | 232 | create_dir(&get_target_path("test_dir4"), true); 233 | 234 | let scp = session.open_scp().unwrap(); 235 | scp.download("./", "test_dir4").unwrap(); 236 | remove_dir("test_dir4"); 237 | remove_dir(&get_target_path("test_dir4")); 238 | 239 | session.close(); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 0.5.0 --------------------------------------------------------------------------------