better api

This commit is contained in:
EthanShoeDev
2025-09-13 23:22:51 -04:00
parent 09e4b089f9
commit a51ee62437
4 changed files with 467 additions and 132 deletions

View File

@@ -41,7 +41,8 @@
"typecheck": "tsc", "typecheck": "tsc",
"lint": "eslint \"**/*.{js,ts,tsx}\"", "lint": "eslint \"**/*.{js,ts,tsx}\"",
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
"release": "release-it --only-version" "release": "release-it --only-version",
"lint:rust": "cd rust/uniffi-russh && just lint"
}, },
"keywords": [ "keywords": [
"react-native", "react-native",

View File

@@ -0,0 +1,261 @@
# uniffi-russh
UniFFI bindings around [`russh`](https://github.com/Eugeny/russh), designed to make a **safe, async SSH client** usable from JavaScript/TypeScript — especially **React Native (Hermes/JSI)** — while staying close to russhs model.
This crate hosts the Rust code and UniFFI exports; the React Native module that consumes it lives in `@fressh/react-native-uniffi-russh`.
---
## Why this exists
* The React Native SSH landscape is thin and often not truly async.
* We want a **thin, principled wrapper** over russh that:
* exposes **connection + channel (shell) primitives**;
* streams **binary data chunks** (stdout/stderr) to JS;
* reports **connection state transitions** (TCP and shell);
* provides **keypair generation** via `russh-keys`;
* compiles cleanly for **Android (NDK)** and other targets with **Tokio**.
---
## High-level design
* **Rust core** (this crate): Tokio-based client using `russh` and `russh-keys`. Exposes functions/objects via UniFFI macros.
* **FFI surface** (stable across languages):
* `connect(details, connect_status_listener?) -> SSHConnection`
* `SSHConnection.start_shell(pty, shell_status_listener?) -> channel_id`
* `SSHConnection.send_data(bytes)`
* `SSHConnection.exec(command)`
* `SSHConnection.close_shell()`
* `SSHConnection.disconnect()`
* `generate_key_pair(key_type) -> String (OpenSSH PEM)`
* **Events**:
* `connect_status_listener`: `TcpConnecting``TcpConnected``TcpDisconnected`
* `shell_status_listener`: `ShellConnecting``ShellConnected``ShellDisconnected`
* **Streaming**:
* Register multiple `ChannelListener`s with `SSHConnection.add_channel_listener/remove_channel_listener`.
* Each listener gets `on_data(Vec<u8>)` for stdout/stderr frames (no OSC parsing; renderer decides).
We intentionally keep **shell start** separate from **connect** so app UIs can render intermediate states and so advanced consumers can open multiple channels.
---
## Whats exported (conceptual API)
### Records & Enums
```rust
// Authentication choice
enum Security {
Password { password: String },
Key { key_id: String }, // (planned for auth; keygen is available)
}
// Connection details
record ConnectionDetails {
host: String,
port: u16,
username: String,
security: Security,
}
// Status events
enum SSHConnectionStatus {
TcpConnecting,
TcpConnected,
TcpDisconnected,
ShellConnecting,
ShellConnected,
ShellDisconnected,
}
// PTY selection (maps to SSH pty term names)
enum PtyType { Vanilla, Vt100, Vt102, Vt220, Ansi, Xterm }
// Key generation
enum KeyType { Rsa, Ecdsa, Ed25519, Ed448 /* unsupported */ }
```
### Objects & Traits
```rust
// Rust -> JS callback traits
trait StatusListener { fn on_status_change(status: SSHConnectionStatus); }
trait ChannelListener { fn on_data(data: Vec<u8>); }
// Main connection object
object SSHConnection {
// read-only getters
fn connection_details() -> ConnectionDetails;
fn created_at_ms() -> f64;
fn tcp_established_at_ms() -> f64;
// channel streaming
fn add_channel_listener(listener: Arc<dyn ChannelListener>);
fn remove_channel_listener(listener: Arc<dyn ChannelListener>);
// shell lifecycle (optional; call only if you want a shell)
async fn start_shell(pty: PtyType, shell_status: Option<Arc<dyn StatusListener>>) -> u32; // channel_id
async fn close_shell();
// writing
async fn send_data(bytes: Vec<u8>);
async fn exec(command: String);
// connection lifecycle
async fn disconnect();
}
// top-level
async fn connect(details: ConnectionDetails, connect_status: Option<Arc<dyn StatusListener>>)
-> Arc<SSHConnection>;
async fn generate_key_pair(key_type: KeyType) -> String; // OpenSSH PEM
```
### Error model
All fallible functions return `Result<_, SshError>`. We map errors from `russh`, `russh-keys`, `ssh-key`, and `std::io` into a single enum that UniFFI can surface to JS.
---
## React Native (TypeScript) usage
> This assumes youre using the companion package `@fressh/react-native-uniffi-russh` which wires this crate through UniFFI + JSI for React Native.
```ts
import {
connect,
generateKeyPair,
PtyType,
type ConnectionDetails,
type SSHConnectionStatus,
type SSHConnection,
} from '@fressh/react-native-uniffi-russh';
const details: ConnectionDetails = {
host: 'example.com',
port: 22,
username: 'me',
security: { Password: { password: 'secret' } },
};
const connStatus = {
on_status_change(status: SSHConnectionStatus) {
console.log('connect status:', status);
},
};
const shellStatus = {
on_status_change(status: SSHConnectionStatus) {
console.log('shell status:', status);
},
};
const channelListener = {
on_data(data: Uint8Array) {
// bytes → feed your terminal emulator / decoder
console.log('got', data.length, 'bytes');
},
};
(async () => {
const conn: SSHConnection = await connect(details, connStatus);
// streaming callbacks
conn.add_channel_listener(channelListener);
// optionally start a shell
const chanId = await conn.start_shell(PtyType.Xterm, shellStatus);
console.log('shell channel id', chanId);
// write to the shell
await conn.send_data(new TextEncoder().encode('echo hello\n'));
// or run a one-shot exec request
await conn.exec('uname -a');
// later…
await conn.close_shell();
await conn.disconnect();
})();
```
Key generation:
```ts
const pem = await generateKeyPair('Ed25519'); // string (OpenSSH format)
```
> **Note:** For now, *password* authentication is implemented. Public-key auth is on the roadmap. Key generation works today.
---
## Building & platforms
* **Tokio** runtime (multi-thread) is used for all async.
* This crate is meant to be consumed via **UniFFI**:
* For React Native, we use `uniffi-bindgen-react-native` (UBRN) to generate the TypeScript/JS glue and JSI/Hermes bindings.
* **Android:** Requires NDK; our `russh` dependency is configured to use the `ring` crypto backend (no CMake-heavy `aws-lc`).
* **iOS / Desktop:** The core crate is platform-agnostic; UBRN/JSI wiring is what determines where you can use it from JS.
---
## Staying close to russh
We intentionally keep the shape near russhs primitives:
* **Connect** returns a `SSHConnection` backed by a `russh::client::Handle`.
* **Shell** is an **optional channel** you can start on demand (`start_shell`), with a PTY term (e.g., `xterm-256color`).
* **Multiple channels** are possible (russh supports it). The exported surface focuses on the **typical single shell** flow. Advanced multiplexing is a natural extension.
What we **dont** do here:
* Terminal emulation or OSC parsing (leave that to your renderer).
* SFTP (out of scope for this crate).
* Key agent / forwarding / port forwarding (not yet).
---
## Feature flags / deps (summary)
* `tokio` (rt-multi-thread, macros, time, net, sync)
* `russh` with the **`ring`** crypto backend (to avoid `aws-lc-sys`/CMake churn on Android)
* `russh-keys` for key handling + PEM export
* `thiserror` for error ergonomics
* `rand` (keygen), `bytes`, `futures`, `once_cell` as needed
* `uniffi`/`uniffi_macros` for the FFI surface
---
## Roadmap
* Public key authentication (using `russh-keys` and servers supported hash algs)
* Channel multiplexing helpers (e.g., open arbitrary exec channels alongside shell)
* Port forwarding
* Optional OSC133-ish event surfacing (if we later add a parser utility crate)
* SFTP (in a separate crate)
---
## Contributing
We aim for:
* **Predictable FFI surface** (stable enums/records)
* **Tokio-friendly** code (no blocking in async)
* **Clippy-clean** builds (`-D warnings`)
* **Thin abstraction** over russh (no surprises)
PRs welcome — especially improvements to error mapping and additional authentication modes.
---
## License
Same as russh unless otherwise noted in this repository. (Check the repo root for the definitive license file.)

View File

@@ -4,7 +4,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use thiserror::Error; use thiserror::Error;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex as AsyncMutex; use tokio::sync::Mutex as AsyncMutex;
use russh::{self, client, ChannelMsg, Disconnect}; use russh::{self, client, ChannelMsg, Disconnect};
@@ -14,12 +13,12 @@ use russh_keys::ssh_key::{self, LineEnding};
uniffi::setup_scaffolding!(); uniffi::setup_scaffolding!();
/// ---------- Types mirroring your TS shape ---------- /// ---------- Types ----------
#[derive(Debug, Clone, PartialEq, uniffi::Enum)] #[derive(Debug, Clone, PartialEq, uniffi::Enum)]
pub enum Security { pub enum Security {
Password { password: String }, Password { password: String },
Key { key_id: String }, // left unimplemented in connect() for now Key { key_id: String }, // (key-based auth can be wired later)
} }
#[derive(Debug, Clone, PartialEq, uniffi::Record)] #[derive(Debug, Clone, PartialEq, uniffi::Record)]
@@ -40,25 +39,46 @@ pub enum SSHConnectionStatus {
ShellDisconnected, ShellDisconnected,
} }
/// PTY types similar to the old TS lib (plus xterm-256color, which is common).
#[derive(Debug, Clone, Copy, PartialEq, uniffi::Enum)]
pub enum PtyType {
Vanilla,
Vt100,
Vt102,
Vt220,
Ansi,
Xterm,
Xterm256,
}
impl PtyType {
fn as_ssh_name(self) -> &'static str {
match self {
PtyType::Vanilla => "vanilla",
PtyType::Vt100 => "vt100",
PtyType::Vt102 => "vt102",
PtyType::Vt220 => "vt220",
PtyType::Ansi => "ansi",
PtyType::Xterm => "xterm",
PtyType::Xterm256 => "xterm-256color",
}
}
}
#[derive(Debug, Error, uniffi::Error)] #[derive(Debug, Error, uniffi::Error)]
pub enum SshError { pub enum SshError {
#[error("Disconnected")] #[error("Disconnected")]
Disconnected, Disconnected,
#[error("Unsupported key type")] #[error("Unsupported key type")]
UnsupportedKeyType, UnsupportedKeyType,
#[error("Auth failed: {0}")] #[error("Auth failed: {0}")]
Auth(String), Auth(String),
#[error("Shell already running")]
ShellAlreadyRunning,
#[error("russh error: {0}")] #[error("russh error: {0}")]
Russh(String), Russh(String),
#[error("russh-keys error: {0}")] #[error("russh-keys error: {0}")]
RusshKeys(String), RusshKeys(String),
} }
// Allow `?` on various fallible calls:
impl From<russh::Error> for SshError { impl From<russh::Error> for SshError {
fn from(e: russh::Error) -> Self { SshError::Russh(e.to_string()) } fn from(e: russh::Error) -> Self { SshError::Russh(e.to_string()) }
} }
@@ -68,19 +88,16 @@ impl From<russh_keys::Error> for SshError {
impl From<ssh_key::Error> for SshError { impl From<ssh_key::Error> for SshError {
fn from(e: ssh_key::Error) -> Self { SshError::RusshKeys(e.to_string()) } fn from(e: ssh_key::Error) -> Self { SshError::RusshKeys(e.to_string()) }
} }
impl From<std::io::Error> for SshError {
fn from(e: std::io::Error) -> Self { SshError::Russh(e.to_string()) }
}
/// Status callback from Rust -> JS /// Status callback (used separately by connect and by start_shell)
#[uniffi::export(with_foreign)] #[uniffi::export(with_foreign)]
pub trait StatusListener: Send + Sync { pub trait StatusListener: Send + Sync {
fn on_status_change(&self, status: SSHConnectionStatus); fn on_status_change(&self, status: SSHConnectionStatus);
} }
/// Data callback from Rust -> JS (stdout/stderr chunks unified) /// Channel data callback (stdout/stderr unified)
#[uniffi::export(with_foreign)] #[uniffi::export(with_foreign)]
pub trait DataListener: Send + Sync { pub trait ChannelListener: Send + Sync {
fn on_data(&self, data: Vec<u8>); fn on_data(&self, data: Vec<u8>);
} }
@@ -93,21 +110,30 @@ pub enum KeyType {
Ed448, Ed448,
} }
/// ---------- Connection object ---------- /// ---------- Connection object (no shell until start_shell) ----------
#[derive(uniffi::Object)] #[derive(uniffi::Object)]
pub struct SSHConnection { pub struct SSHConnection {
connection_details: ConnectionDetails, connection_details: ConnectionDetails,
session_id: String,
created_at_ms: f64, created_at_ms: f64,
established_at_ms: f64, tcp_established_at_ms: f64,
listeners: Mutex<Vec<Arc<dyn DataListener>>>,
// write side for sending stdin to the shell
writer: AsyncMutex<russh::ChannelWriteHalf<client::Msg>>,
// handle is kept so we can call disconnect
handle: AsyncMutex<ClientHandle<NoopHandler>>, handle: AsyncMutex<ClientHandle<NoopHandler>>,
// Shell state (one active shell per connection by design).
shell: AsyncMutex<Option<ShellState>>,
// Data listeners for whatever shell is active.
listeners: Arc<Mutex<Vec<Arc<dyn ChannelListener>>>>,
}
struct ShellState {
channel_id: u32,
writer: russh::ChannelWriteHalf<client::Msg>,
// We keep the reader task to allow cancellation on close.
reader_task: tokio::task::JoinHandle<()>,
// Only used for Shell* statuses.
shell_status_listener: Option<Arc<dyn StatusListener>>,
} }
impl fmt::Debug for SSHConnection { impl fmt::Debug for SSHConnection {
@@ -115,57 +141,148 @@ impl fmt::Debug for SSHConnection {
let listeners_len = self.listeners.lock().map(|v| v.len()).unwrap_or(0); let listeners_len = self.listeners.lock().map(|v| v.len()).unwrap_or(0);
f.debug_struct("SSHConnection") f.debug_struct("SSHConnection")
.field("connection_details", &self.connection_details) .field("connection_details", &self.connection_details)
.field("session_id", &self.session_id)
.field("created_at_ms", &self.created_at_ms) .field("created_at_ms", &self.created_at_ms)
.field("established_at_ms", &self.established_at_ms) .field("tcp_established_at_ms", &self.tcp_established_at_ms)
.field("listeners_len", &listeners_len) .field("listeners_len", &listeners_len)
.finish() .finish()
} }
} }
/// Put the UniFFI export attribute on the IMPL BLOCK (not individual methods).
#[uniffi::export(async_runtime = "tokio")]
impl SSHConnection {
pub fn connection_details(&self) -> ConnectionDetails {
self.connection_details.clone()
}
pub fn session_id(&self) -> String {
self.session_id.clone()
}
pub fn created_at_ms(&self) -> f64 {
self.created_at_ms
}
pub fn established_at_ms(&self) -> f64 {
self.established_at_ms
}
pub fn add_data_listener(&self, listener: Arc<dyn DataListener>) {
self.listeners.lock().unwrap().push(listener);
}
/// Send bytes to the remote shell (stdin).
pub async fn send_data(&self, data: Vec<u8>) -> Result<(), SshError> {
// ChannelWriteHalf isnt AsyncWrite. Use a one-shot writer facade.
let writer = self.writer.lock().await;
let mut stream = writer.make_writer();
stream.write_all(&data).await?;
stream.flush().await?;
Ok(())
}
/// Graceful disconnect.
pub async fn disconnect(&self) -> Result<(), SshError> {
let handle = self.handle.lock().await;
handle.disconnect(Disconnect::ByApplication, "bye", "").await?;
Ok(())
}
}
/// Minimal client::Handler. /// Minimal client::Handler.
struct NoopHandler; struct NoopHandler;
impl client::Handler for NoopHandler { impl client::Handler for NoopHandler {
type Error = SshError; type Error = SshError;
// No overrides needed; defaults are fine. }
/// ---------- Methods ----------
#[uniffi::export(async_runtime = "tokio")]
impl SSHConnection {
pub fn connection_details(&self) -> ConnectionDetails {
self.connection_details.clone()
}
pub fn created_at_ms(&self) -> f64 {
self.created_at_ms
}
pub fn tcp_established_at_ms(&self) -> f64 {
self.tcp_established_at_ms
}
/// Return current shell channel id, if any.
pub async fn channel_id(&self) -> Option<u32> {
self.shell.lock().await.as_ref().map(|s| s.channel_id)
}
pub fn add_channel_listener(&self, listener: Arc<dyn ChannelListener>) {
self.listeners.lock().unwrap().push(listener);
}
pub fn remove_channel_listener(&self, listener: Arc<dyn ChannelListener>) {
if let Ok(mut v) = self.listeners.lock() {
v.retain(|l| !Arc::ptr_eq(l, &listener));
}
}
/// Start a shell with the given PTY. Emits only Shell* statuses via `shell_status_listener`.
pub async fn start_shell(
&self,
pty: PtyType,
shell_status_listener: Option<Arc<dyn StatusListener>>,
) -> Result<u32, SshError> {
// Prevent double-start (safe default).
if self.shell.lock().await.is_some() {
return Err(SshError::ShellAlreadyRunning);
}
if let Some(sl) = shell_status_listener.as_ref() {
sl.on_status_change(SSHConnectionStatus::ShellConnecting);
}
// Open session channel.
let handle = self.handle.lock().await;
let ch = handle.channel_open_session().await?;
let channel_id: u32 = ch.id().into();
// Request PTY & shell.
ch.request_pty(true, pty.as_ssh_name(), 80, 24, 0, 0, &[]).await?;
ch.request_shell(true).await?;
// Split for read/write; spawn reader.
let (mut reader, writer) = ch.split();
let listeners = self.listeners.clone();
let shell_listener_for_task = shell_status_listener.clone();
let reader_task = tokio::spawn(async move {
loop {
match reader.wait().await {
Some(ChannelMsg::Data { data }) => {
if let Ok(cl) = listeners.lock() {
let snapshot = cl.clone();
let buf = data.to_vec();
for l in snapshot { l.on_data(buf.clone()); }
}
}
Some(ChannelMsg::ExtendedData { data, .. }) => {
if let Ok(cl) = listeners.lock() {
let snapshot = cl.clone();
let buf = data.to_vec();
for l in snapshot { l.on_data(buf.clone()); }
}
}
Some(ChannelMsg::Close) | None => {
if let Some(sl) = shell_listener_for_task.as_ref() {
sl.on_status_change(SSHConnectionStatus::ShellDisconnected);
}
break;
}
_ => { /* ignore others */ }
}
}
});
*self.shell.lock().await = Some(ShellState {
channel_id,
writer,
reader_task,
shell_status_listener,
});
// Report ShellConnected.
if let Some(sl) = self.shell.lock().await.as_ref().and_then(|s| s.shell_status_listener.clone()) {
sl.on_status_change(SSHConnectionStatus::ShellConnected);
}
Ok(channel_id)
}
/// Send bytes to the active shell (stdin).
pub async fn send_data(&self, data: Vec<u8>) -> Result<(), SshError> {
let mut guard = self.shell.lock().await;
let state = guard.as_mut().ok_or(SshError::Disconnected)?;
state.writer.data(&data[..]).await?;
Ok(())
}
/// Close the active shell channel (if any) and stop its reader task.
pub async fn close_shell(&self) -> Result<(), SshError> {
if let Some(state) = self.shell.lock().await.take() {
// Try to close channel gracefully; ignore error.
state.writer.close().await.ok();
state.reader_task.abort();
if let Some(sl) = state.shell_status_listener {
sl.on_status_change(SSHConnectionStatus::ShellDisconnected);
}
}
Ok(())
}
/// Disconnect TCP (also closes any active shell).
pub async fn disconnect(&self) -> Result<(), SshError> {
// Close shell first.
let _ = self.close_shell().await;
let h = self.handle.lock().await;
h.disconnect(Disconnect::ByApplication, "bye", "").await?;
Ok(())
}
} }
/// ---------- Top-level API ---------- /// ---------- Top-level API ----------
@@ -173,18 +290,22 @@ impl client::Handler for NoopHandler {
#[uniffi::export(async_runtime = "tokio")] #[uniffi::export(async_runtime = "tokio")]
pub async fn connect( pub async fn connect(
details: ConnectionDetails, details: ConnectionDetails,
status_listener: Arc<dyn StatusListener>, connect_status_listener: Option<Arc<dyn StatusListener>>,
) -> Result<Arc<SSHConnection>, SshError> { ) -> Result<Arc<SSHConnection>, SshError> {
status_listener.on_status_change(SSHConnectionStatus::TcpConnecting); if let Some(sl) = connect_status_listener.as_ref() {
sl.on_status_change(SSHConnectionStatus::TcpConnecting);
}
// connect(config, addr, handler) // TCP
let cfg = Arc::new(ClientConfig::default()); let cfg = Arc::new(ClientConfig::default());
let addr = format!("{}:{}", details.host, details.port); let addr = format!("{}:{}", details.host, details.port);
let mut handle: ClientHandle<NoopHandler> = client::connect(cfg, addr, NoopHandler).await?; let mut handle: ClientHandle<NoopHandler> = client::connect(cfg, addr, NoopHandler).await?;
status_listener.on_status_change(SSHConnectionStatus::TcpConnected); if let Some(sl) = connect_status_listener.as_ref() {
sl.on_status_change(SSHConnectionStatus::TcpConnected);
}
// authenticate // Auth
let auth = match &details.security { let auth = match &details.security {
Security::Password { password } => { Security::Password { password } => {
handle.authenticate_password(details.username.clone(), password.clone()).await? handle.authenticate_password(details.username.clone(), password.clone()).await?
@@ -198,71 +319,20 @@ pub async fn connect(
other => return Err(SshError::Auth(format!("{other:?}"))), other => return Err(SshError::Auth(format!("{other:?}"))),
} }
status_listener.on_status_change(SSHConnectionStatus::ShellConnecting);
// open session, request pty, launch shell
let ch = handle.channel_open_session().await?;
ch.request_pty(true, "xterm-256color", 80, 24, 0, 0, &[]).await?;
ch.request_shell(true).await?;
status_listener.on_status_change(SSHConnectionStatus::ShellConnected);
// split for read/write
let (mut reader, writer) = ch.split();
// build the connection object
let now = now_ms(); let now = now_ms();
let conn = Arc::new(SSHConnection { Ok(Arc::new(SSHConnection {
connection_details: details.clone(), connection_details: details,
session_id: format!("session-{}", now as u64),
created_at_ms: now, created_at_ms: now,
established_at_ms: now, tcp_established_at_ms: now,
listeners: Mutex::new(Vec::new()),
writer: AsyncMutex::new(writer),
handle: AsyncMutex::new(handle), handle: AsyncMutex::new(handle),
}); shell: AsyncMutex::new(None),
listeners: Arc::new(Mutex::new(Vec::new())),
// background read loop: forward stdout/stderr chunks }))
let weak = Arc::downgrade(&conn);
tokio::spawn(async move {
loop {
match reader.wait().await {
Some(ChannelMsg::Data { data }) => {
if let Some(conn) = weak.upgrade() {
let listeners = conn.listeners.lock().unwrap().clone();
let buf = data.to_vec();
for l in listeners { l.on_data(buf.clone()); }
} else { break; }
}
Some(ChannelMsg::ExtendedData { data, .. }) => {
if let Some(conn) = weak.upgrade() {
let listeners = conn.listeners.lock().unwrap().clone();
let buf = data.to_vec();
for l in listeners { l.on_data(buf.clone()); }
} else { break; }
}
Some(ChannelMsg::ExitStatus { .. }) => {
// Optionally store/report exit status here.
}
Some(ChannelMsg::Close) | None => {
if let Some(_conn) = weak.upgrade() {
status_listener.on_status_change(SSHConnectionStatus::ShellDisconnected);
status_listener.on_status_change(SSHConnectionStatus::TcpDisconnected);
}
break;
}
_ => { /* ignore others for now */ }
}
}
});
Ok(conn)
} }
#[uniffi::export(async_runtime = "tokio")] #[uniffi::export(async_runtime = "tokio")]
pub async fn generate_key_pair(key_type: KeyType) -> Result<String, SshError> { pub async fn generate_key_pair(key_type: KeyType) -> Result<String, SshError> {
let mut rng = OsRng; let mut rng = OsRng;
let key = match key_type { let key = match key_type {
KeyType::Rsa => PrivateKey::random(&mut rng, KeyAlgorithm::Rsa { hash: None })?, KeyType::Rsa => PrivateKey::random(&mut rng, KeyAlgorithm::Rsa { hash: None })?,
KeyType::Ecdsa => PrivateKey::random( KeyType::Ecdsa => PrivateKey::random(
@@ -272,9 +342,7 @@ pub async fn generate_key_pair(key_type: KeyType) -> Result<String, SshError> {
KeyType::Ed25519 => PrivateKey::random(&mut rng, KeyAlgorithm::Ed25519)?, KeyType::Ed25519 => PrivateKey::random(&mut rng, KeyAlgorithm::Ed25519)?,
KeyType::Ed448 => return Err(SshError::UnsupportedKeyType), KeyType::Ed448 => return Err(SshError::UnsupportedKeyType),
}; };
let pem = key.to_openssh(LineEnding::LF)?; // Zeroizing<String>
// OpenSSH PEM, LF endings; returns Zeroizing<String>
let pem = key.to_openssh(LineEnding::LF)?;
Ok(pem.to_string()) Ok(pem.to_string())
} }

View File

@@ -2,11 +2,16 @@
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"tasks": { "tasks": {
"build": { "build": {
"outputs": ["lib/**", "android/**", "ios/**", "cpp/**", "src/**"] "outputs": ["lib/**", "android/**", "ios/**", "cpp/**", "src/**"],
"dependsOn": ["lint:rust"]
}, },
"typecheck": { "typecheck": {
"dependsOn": ["build"] "dependsOn": ["build"]
} },
"lint": {
"with": ["typecheck", "lint:rust"]
},
"lint:rust": {}
}, },
"extends": ["//"] "extends": ["//"]
} }