Add uname, /uname and scp (key or masked password) in the Shell
CI / build (pull_request) Failing after 1m59s
Site / build (pull_request) Successful in 9s

- uname in the console and /uname in IRC: the firmware, its version and the chip
- scp: one file between the card and a trusted server, over SSH, with the
  device's key or a password asked (masked) in the Shell
- shared libssh helpers in src/platform/libssh_util
- README, user guide, command reference and a how-to updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-09 19:01:36 +02:00
co-authored by Claude Sonnet 5.5
parent e707a5f2d4
commit 18285c3212
25 changed files with 730 additions and 34 deletions
+32 -1
View File
@@ -33,6 +33,9 @@ void ShellApp::onEnter() {
// Nothing is kept once it's left: the ring, the lines and the list of commands all go (Q207).
void ShellApp::onExit() {
if (password_ && abort) abort();
password_ = false;
input_.setText("");
console.closeShellRing();
console.shellShowsAll(false);
open_ = false;
@@ -49,6 +52,11 @@ void ShellApp::update(uint32_t) {
if (skipped) log_.add("[... " + std::to_string(skipped) + " bytes lost: more was printed than fits]");
log_.feed(reinterpret_cast<const char*>(buf), n);
}
if (!password_ && asking && asking()) { // after the question has been printed above
password_ = true;
input_.setText("");
requestRedraw();
}
if (log_.revision() != seenRevision_) {
seenRevision_ = log_.revision();
requestRedraw();
@@ -114,6 +122,23 @@ bool ShellApp::onKey(const KeyEvent& e) {
log_.add(console.shellShowsAll() ? "Showing everything the console prints." : "Showing only the replies to your commands.");
return true;
}
if (password_) { // masked, kept nowhere: not in the history, not in the log
switch (e.key) {
case Key::Char: input_.insert(e.ch); break;
case Key::Delete: input_.backspace(); break;
case Key::Select: {
std::string typed = input_.text();
input_.setText("");
password_ = false;
log_.add("> " + std::string(typed.size(), '*'));
if (answer) answer(typed);
typed.assign(typed.size(), '\0');
break;
}
default: return false; // Back leaves the Shell, which cancels the copy
}
return true;
}
std::string recalled;
switch (e.key) {
case Key::Char: input_.insert(e.ch); break;
@@ -197,7 +222,13 @@ void ShellApp::draw(Canvas& c) {
}
c.setTextDatum(top_left);
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
if (password_) {
LineEditor stars(240);
stars.setText(std::string(input_.text().size(), '*'));
widgets::lineEditor(c, stars, {2, area.y + area.h - inputH, area.w - 4, 0});
} else {
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
}
if (confirm_) widgets::dialog(c, "Delete?", question_, *confirm_);
}
+7
View File
@@ -36,6 +36,12 @@ class ShellApp : public App {
ShellApp(Run run, Probe probe, List list, Count count, const char* helpText, AppManager& apps)
: run_(std::move(run)), probe_(std::move(probe)), list_(std::move(list)), count_(std::move(count)), helpText_(helpText), apps_(apps) {}
// `scp` asking for a password: while asking() is true, the next line typed is masked and goes to
// answer() instead of the console; leaving the Shell calls abort().
std::function<bool()> asking;
std::function<void(const std::string&)> answer;
std::function<void()> abort;
void onEnter() override;
void onExit() override;
bool onKey(const KeyEvent& e) override;
@@ -63,6 +69,7 @@ class ShellApp : public App {
uint32_t ringPos_ = 0, seenRevision_ = 0;
int scroll_ = 0; // wrapped lines scrolled back from the bottom
bool open_ = false;
bool password_ = false; // the line being typed is a password
};
} // namespace roro
+26 -2
View File
@@ -14,6 +14,7 @@
#include "apps/shell_app.h"
#include "apps/ssh_app.h"
#include "services/net_tools.h"
#include "services/scp_service.h"
#include "services/ssh_service.h"
#include "services/vpn_service.h"
#include "services/web_share.h"
@@ -63,6 +64,7 @@
#include "storage_paths.h"
#include "ui/notifier.h"
#include "ui/screen.h"
#include "scp_args.h"
#include "version.h"
using namespace roro;
@@ -95,6 +97,8 @@ static VpnService* vpnService; // not in Safe Mode
static NetTools netTools; // ping, nslookup and the rest (issue #90)
static SshService* sshService; // not in Safe Mode (issue #2)
static SshApp* sshApp;
static ShellApp* shellApp;
static ScpService* scpService; // not in Safe Mode
static Notifier* notifier;
static LauncherApp launcher;
static AppManager* apps;
@@ -235,8 +239,13 @@ void setup() {
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
sshService = new SshService(settings);
sshApp = new SshApp(*sshService, settings, *storageService);
scpService = new ScpService(settings, *storageService, *fileOps);
apps->registerApp({"ssh", "SSH", false, sshApp});
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, shellProbe, shellList, shellCount, helpText(), *apps)});
shellApp = new ShellApp(shellRun, shellProbe, shellList, shellCount, helpText(), *apps);
apps->registerApp({"shell", "Shell", false, shellApp});
shellApp->asking = [] { return scpService->asking(); };
shellApp->answer = [](const std::string& password) { scpService->answer(password); };
shellApp->abort = [] { scpService->cancel(); };
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
power->beforePowerOff = []() { apps->home(); };
netTools.ntpServer = []() { return settings.getString(Setting::Ntp1); };
@@ -688,6 +697,8 @@ static const char* const kHelp =
"ping <host> [count] [size] | nslookup <name> [server] | port <host> <port> | traceroute <host> | cancel is it there, does its name resolve, is its port open, which way; one at a time\n"
"tls <host> [port] | ntp [server] a TLS handshake: who the certificate is for, by whom, until when, and whether this device trusts it; a time server's clock against this one\n"
"ifconfig | arp | netstat the interfaces (Wi-Fi and the VPN), their addresses, the default route and the DNS servers; the neighbours heard; what listens and what is connected\n"
"uname the firmware, its version and the chip it is built for\n"
"scp [-f] [-P port] <file on the card> user@host:path | scp [-f] [-P port] user@host:path <file on the card> one file over SSH, with this device's key or, in the Shell, a password, to a server the SSH App already trusts; -f replaces a file on the card; `cancel` stops it\n"
"ssh user@host[:port] | ssh status | ssh stop a terminal on another machine, in the SSH App; the password is asked there, never here\n"
"vpn status | vpn up [seconds] | vpn down | vpn import [path] | vpn forget | vpn auto on|off the WireGuard tunnel (Settings > VPN); import reads /vpn/wg0.conf; with seconds, it goes down by itself\n"
"debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\n"
@@ -771,6 +782,16 @@ static void sshCommand(const String& arg) {
}
}
// `scp ...`: see ScpService. It needs the key and the trusted server the SSH App sets up.
static void scpCommand(const String& arg) {
if (!scpService) return (void)console.println("scp: not available in Safe Mode");
term::ScpArgs args;
std::string why = term::parseScp(arg.c_str(), args);
if (!why.empty()) return (void)console.printf("scp: %s\n", why.c_str());
why = scpService->start(args, sshService && sshService->state() != SshService::State::Idle && sshService->state() != SshService::State::Ended);
if (!why.empty()) console.printf("scp: error %s\n", why.c_str());
}
// `vpn ...` (issue #8). Nothing here prints a key.
static void vpnCommand(const String& arg) {
if (!vpnService) return (void)console.println("vpn: not available in Safe Mode");
@@ -840,7 +861,7 @@ static void screenshotStep() {
// Commands that only touch what Safe Mode starts.
static bool safeModeCommand(const String& line) {
return line == "help" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" ||
return line == "help" || line == "uname" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" ||
line.startsWith("log level ") || line.startsWith("crash") || line.startsWith("coredump") ||
line == "wifi status" || line.startsWith("wifi add ") || line.startsWith("debug ");
}
@@ -905,6 +926,9 @@ static void runCommand(String line, bool fromSerial = false) {
}
if (netTools.command(line.c_str())) return;
if (line == "cancel") netTools.cancel(); // and a copy or a delete, below
if (line == "uname" || line == "uname -a") return (void)console.println(unameString().c_str());
if (line.startsWith("scp ")) return scpCommand(line.substring(4));
if (line == "cancel" && scpService) scpService->cancel();
if (line == "ssh" || line.startsWith("ssh ")) return sshCommand(line.length() > 4 ? line.substring(4) : String("status"));
if (line == "vpn" || line.startsWith("vpn ")) return vpnCommand(line.length() > 4 ? line.substring(4) : String("status"));
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
+29
View File
@@ -0,0 +1,29 @@
#include "platform/libssh_util.h"
#include <libssh_esp32.h>
namespace roro::sshutil {
std::string fingerprintOf(ssh_session s) {
ssh_key key = nullptr;
unsigned char* hash = nullptr;
size_t len = 0;
std::string out;
if (ssh_get_server_publickey(s, &key) == SSH_OK && ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_SHA256, &hash, &len) == SSH_OK) {
if (char* text = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256, hash, len)) {
out = text; // "SHA256:..."
ssh_string_free_char(text);
}
ssh_clean_pubkey_hash(&hash);
}
if (key) ssh_key_free(key);
return out;
}
void startLibrary() {
static bool started = false;
if (!started) libssh_begin();
started = true;
}
} // namespace roro::sshutil
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <libssh/libssh.h>
// What the SSH Service and the Scp Service both do with libssh.
namespace roro::sshutil {
// libssh_begin(), once.
void startLibrary();
// "SHA256:..." of the key the server showed, or "".
std::string fingerprintOf(ssh_session s);
} // namespace roro::sshutil
+269
View File
@@ -0,0 +1,269 @@
#include "services/scp_service.h"
#include <Arduino.h>
#include <SD.h>
#include <algorithm>
#include <libssh/libssh.h>
#include "platform/libssh_util.h"
namespace roro {
namespace {
constexpr uint32_t kStack = 20480; // as the SSH Service's
constexpr size_t kPiece = 4096;
std::string baseName(const std::string& path) {
size_t slash = path.rfind('/');
return slash == std::string::npos ? path : path.substr(slash + 1);
}
} // namespace
struct ScpService::Job {
ScpService* owner;
Console::Origin from;
term::ScpArgs args;
std::string privateKey, known;
bool stopped() const { return owner->stop_; }
bool onCard(const std::function<void()>& work) { return owner->storage_.runAndWait(work); }
void run();
bool login(ssh_session s);
void upload(ssh_session s);
void download(ssh_session s);
};
std::string ScpService::start(const term::ScpArgs& args, bool sshOpen) {
if (busy_) return "A copy is running: `cancel` stops it";
if (sshOpen) return "An SSH session is open: close it first, there isn't memory for both";
if (ESP.getFreeHeap() < kNeedFree) return "Not enough memory: close IRC or a Gemini page";
std::string key = settings_.getString(Setting::SshKey);
if (!args.upload) {
if (std::string why = files_.whyReadOnly(args.local, false); !why.empty()) return why;
bool exists = false;
if (!storage_.runAndWait([&]() { exists = SD.exists(args.local.c_str()); })) return "The card isn't answering";
if (exists && !args.replace) return args.local + " exists already: scp -f replaces it";
}
auto* job = new Job{this, console.origin(), args, key, term::SshKnownHosts(settings_.getString(Setting::SshKnown)).fingerprintOf(args.target.hostPort())};
stop_ = asking_ = answered_ = false;
busy_ = true;
if (xTaskCreate(task, "scp", kStack, job, 1, nullptr) != pdPASS) {
busy_ = false;
job->privateKey.assign(job->privateKey.size(), '\0');
delete job;
return "Not enough memory for it";
}
return "";
}
void ScpService::task(void* arg) {
Job* job = static_cast<Job*>(arg);
{
Console::As as(job->from);
job->run();
}
job->privateKey.assign(job->privateKey.size(), '\0');
ScpService* owner = job->owner;
delete job;
owner->busy_ = false;
vTaskDelete(nullptr);
}
void ScpService::answer(const std::string& password) {
if (!asking_) return;
password_ = password;
asking_ = false;
answered_ = true;
}
// This device's key first, then the password, three tries: asked in the Shell that typed the command.
bool ScpService::Job::login(ssh_session s) {
int rc = ssh_userauth_none(s, nullptr);
if (rc == SSH_AUTH_SUCCESS) return true;
int methods = ssh_userauth_list(s, nullptr);
if (!privateKey.empty() && (methods & SSH_AUTH_METHOD_PUBLICKEY)) {
ssh_key key = nullptr;
if (ssh_pki_import_privkey_base64(privateKey.c_str(), nullptr, nullptr, nullptr, &key) == SSH_OK) {
rc = ssh_userauth_publickey(s, nullptr, key);
ssh_key_free(key);
}
}
privateKey.assign(privateKey.size(), '\0');
if (rc == SSH_AUTH_SUCCESS) return true;
if (!(methods & (SSH_AUTH_METHOD_PASSWORD | SSH_AUTH_METHOD_INTERACTIVE))) {
console.println("scp: error the server takes neither this device's key nor a password");
return false;
}
if (from != Console::Origin::Shell) {
console.println("scp: error the server doesn't know this device's key, and a password is only asked in the Shell");
return false;
}
for (int tries = 0; tries < 3 && !stopped(); tries++) {
console.printf("scp: %s's password for %s (Back cancels):\n", args.target.user.c_str(), args.target.host.c_str());
owner->answered_ = false;
owner->asking_ = true;
while (!owner->answered_ && !stopped()) vTaskDelay(pdMS_TO_TICKS(50));
owner->asking_ = false;
if (stopped()) break;
std::string password;
password.swap(owner->password_);
if (methods & SSH_AUTH_METHOD_PASSWORD) rc = ssh_userauth_password(s, nullptr, password.c_str());
else { // asked as questions: every one gets the password
rc = ssh_userauth_kbdint(s, nullptr, nullptr);
for (int round = 0; rc == SSH_AUTH_INFO && round < 4; round++) {
int n = ssh_userauth_kbdint_getnprompts(s);
for (int i = 0; i < n; i++) ssh_userauth_kbdint_setanswer(s, static_cast<unsigned>(i), password.c_str());
rc = ssh_userauth_kbdint(s, nullptr, nullptr);
}
}
password.assign(password.size(), '\0');
if (rc == SSH_AUTH_SUCCESS) return true;
if (rc == SSH_AUTH_ERROR) break;
console.println("scp: wrong password");
}
console.println(stopped() ? "scp: stopped" : "scp: error not logged in");
return false;
}
void ScpService::Job::run() {
sshutil::startLibrary();
ssh_session s = ssh_new();
if (!s) return (void)console.println("scp: error not enough memory");
int verbosity = SSH_LOG_NOLOG, port = args.target.port;
long timeout = 10;
ssh_options_set(s, SSH_OPTIONS_HOST, args.target.host.c_str());
ssh_options_set(s, SSH_OPTIONS_PORT, &port);
ssh_options_set(s, SSH_OPTIONS_USER, args.target.user.c_str());
ssh_options_set(s, SSH_OPTIONS_TIMEOUT, &timeout);
ssh_options_set(s, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
if (ssh_connect(s) != SSH_OK) {
console.printf("scp: error no connection to %s: %s\n", args.target.hostPort().c_str(), ssh_get_error(s));
} else if (known.empty()) {
console.printf("scp: error %s is not a server this device trusts yet: connect with the SSH App once\n", args.target.hostPort().c_str());
} else if (sshutil::fingerprintOf(s) != known) {
console.printf("scp: error %s showed a different key than the one remembered: not connected\n", args.target.hostPort().c_str());
} else if (login(s)) {
if (args.upload) upload(s);
else download(s);
}
ssh_disconnect(s);
ssh_free(s);
}
void ScpService::Job::upload(ssh_session s) {
// The remote path is where the file goes; the name pushed is only used if that is a folder.
ssh_scp scp = ssh_scp_new(s, SSH_SCP_WRITE, args.remote.c_str());
if (!scp || ssh_scp_init(scp) != SSH_OK) {
console.printf("scp: error %s\n", ssh_get_error(s));
if (scp) ssh_scp_free(scp);
return;
}
File f;
size_t size = 0;
bool opened = onCard([&]() {
f = SD.open(args.local.c_str());
if (f && !f.isDirectory()) size = f.size();
else if (f) f.close();
});
if (!opened || !f) {
console.printf("scp: error cannot read %s\n", args.local.c_str());
ssh_scp_close(scp);
ssh_scp_free(scp);
return;
}
console.printf("scp: %s (%u bytes) to %s:%s\n", args.local.c_str(), (unsigned)size, args.target.text().c_str(), args.remote.c_str());
bool ok = ssh_scp_push_file(scp, baseName(args.local).c_str(), size, 0644) == SSH_OK;
uint8_t buf[kPiece];
uint32_t started = millis();
size_t sent = 0;
while (ok && sent < size && !stopped()) {
int n = 0;
onCard([&]() { n = f.read(buf, std::min(sizeof buf, size - sent)); });
if (n <= 0) {
console.printf("scp: error the card stopped reading at %u of %u\n", (unsigned)sent, (unsigned)size);
ok = false;
break;
}
if (ssh_scp_write(scp, buf, static_cast<size_t>(n)) != SSH_OK) {
console.printf("scp: error %s\n", ssh_get_error(s));
ok = false;
break;
}
sent += static_cast<size_t>(n);
}
onCard([&]() { f.close(); });
if (ok && stopped()) console.printf("scp: stopped at %u of %u bytes\n", (unsigned)sent, (unsigned)size);
else if (ok) console.printf("scp: sent %u bytes in %lu ms\n", (unsigned)sent, (unsigned long)(millis() - started));
else if (!ok && sent == 0) console.printf("scp: error the server refused it: %s\n", ssh_get_error(s));
ssh_scp_close(scp);
ssh_scp_free(scp);
}
void ScpService::Job::download(ssh_session s) {
ssh_scp scp = ssh_scp_new(s, SSH_SCP_READ, args.remote.c_str());
if (!scp || ssh_scp_init(scp) != SSH_OK) {
console.printf("scp: error %s\n", ssh_get_error(s));
if (scp) ssh_scp_free(scp);
return;
}
// Into a part file, renamed once it is all there: a failure never leaves half a file under the real name.
std::string part = args.local + ".part";
File f;
bool ok = false;
size_t got = 0, size = 0;
uint32_t started = millis();
if (ssh_scp_pull_request(scp) != SSH_SCP_REQUEST_NEWFILE) {
const char* why = ssh_get_error(s);
console.printf("scp: error %s\n", why && *why ? why : "that isn't a plain file: scp copies one file");
} else {
size = static_cast<size_t>(ssh_scp_request_get_size64(scp));
ssh_scp_accept_request(scp);
console.printf("scp: %s:%s (%u bytes) to %s\n", args.target.text().c_str(), args.remote.c_str(), (unsigned)size, args.local.c_str());
bool opened = false;
onCard([&]() {
size_t slash = args.local.rfind('/');
if (slash > 0 && !SD.exists(args.local.substr(0, slash).c_str())) SD.mkdir(args.local.substr(0, slash).c_str());
if (SD.exists(part.c_str())) SD.remove(part.c_str());
f = SD.open(part.c_str(), FILE_WRITE);
opened = static_cast<bool>(f);
});
if (!opened) console.printf("scp: error cannot write %s\n", args.local.c_str());
ok = opened;
uint8_t buf[kPiece];
while (ok && got < size && !stopped()) {
int n = ssh_scp_read(scp, buf, std::min(sizeof buf, size - got));
if (n <= 0) {
console.printf("scp: error the connection ended at %u of %u bytes\n", (unsigned)got, (unsigned)size);
ok = false;
break;
}
size_t written = 0;
onCard([&]() { written = f.write(buf, static_cast<size_t>(n)); });
if (written != static_cast<size_t>(n)) {
console.println("scp: error the card refused the write: is it full?");
ok = false;
break;
}
got += static_cast<size_t>(n);
}
if (ok && got < size) ok = false, console.printf("scp: stopped at %u of %u bytes\n", (unsigned)got, (unsigned)size);
}
bool renamed = false;
onCard([&]() {
if (f) f.close();
if (ok) {
if (SD.exists(args.local.c_str())) SD.remove(args.local.c_str());
renamed = SD.rename(part.c_str(), args.local.c_str());
} else if (SD.exists(part.c_str())) {
SD.remove(part.c_str());
}
});
if (ok && !renamed) console.println("scp: error the card refused the new name");
else if (ok) console.printf("scp: received %u bytes in %lu ms\n", (unsigned)got, (unsigned long)(millis() - started));
ssh_scp_close(scp);
ssh_scp_free(scp);
}
} // namespace roro
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <atomic>
#include <memory>
#include <string>
#include "platform/console.h"
#include "scp_args.h"
#include "services/file_ops.h"
#include "services/storage_service.h"
#include "settings.h"
namespace roro {
// `scp`: one file between the card and a server, over SSH (libssh's SCP), on a task of its own that
// prints to the console that asked. It logs in with this device's key and, if the server doesn't
// know it, asks for the password: but only when the command was typed in the Shell, which draws
// the question masked (asking(), answer()). It goes only to servers the SSH App has already been
// told to trust. One at a time, and not while an SSH session is open (they don't fit in memory
// together).
class ScpService {
public:
static constexpr size_t kNeedFree = 75 * 1024;
ScpService(Settings& settings, StorageService& storage, FileOps& files) : settings_(settings), storage_(storage), files_(files) {}
// "" when it started, or why not.
std::string start(const term::ScpArgs& args, bool sshOpen);
void cancel() {
stop_ = true;
asking_ = false;
answered_ = true; // whatever it was waiting for
}
bool busy() const { return busy_; }
// The password is wanted (for user@host, in `who`); the Shell calls answer() with what was typed.
bool asking() const { return asking_; }
void answer(const std::string& password);
private:
struct Job;
static void task(void* arg);
Settings& settings_;
StorageService& storage_;
FileOps& files_;
std::atomic<bool> busy_{false}, stop_{false}, asking_{false}, answered_{false};
std::string password_; // written by answer() before answered_ is set, read by the task after
};
} // namespace roro
+5 -24
View File
@@ -5,6 +5,8 @@
#include <libssh_esp32.h>
#include <libssh/libssh.h>
#include "platform/libssh_util.h"
namespace roro {
namespace {
@@ -17,27 +19,6 @@ struct Locked {
SemaphoreHandle_t lock_;
};
std::string fingerprintOf(ssh_session s) {
ssh_key key = nullptr;
unsigned char* hash = nullptr;
size_t len = 0;
std::string out;
if (ssh_get_server_publickey(s, &key) == SSH_OK && ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_SHA256, &hash, &len) == SSH_OK) {
if (char* text = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256, hash, len)) {
out = text; // "SHA256:..."
ssh_string_free_char(text);
}
ssh_clean_pubkey_hash(&hash);
}
if (key) ssh_key_free(key);
return out;
}
void startLibrary() {
static bool started = false;
if (!started) libssh_begin();
started = true;
}
} // namespace
struct SshService::Run {
@@ -170,7 +151,7 @@ void SshService::task(void* arg) {
}
void SshService::session(Run& run) {
startLibrary();
sshutil::startLibrary();
ssh_session s = ssh_new();
if (!s) return end("Not enough memory for the session");
int verbosity = SSH_LOG_NOLOG;
@@ -204,7 +185,7 @@ void SshService::session(Run& run) {
if (stop_) return fail("Stopped");
// Is it the server it was last time? The first time, and when it has changed, the user decides.
std::string seen = fingerprintOf(s);
std::string seen = sshutil::fingerprintOf(s);
if (seen.empty()) return fail("The server showed no key");
if (seen != run.known) {
{
@@ -333,7 +314,7 @@ std::string SshService::makeKey() {
auto work = [](void* p) {
std::shared_ptr<Made> m = *static_cast<std::shared_ptr<Made>*>(p);
delete static_cast<std::shared_ptr<Made>*>(p);
startLibrary();
sshutil::startLibrary();
ssh_key key = nullptr, pub = nullptr;
char *b64 = nullptr, *pub64 = nullptr;
if (ssh_pki_generate(SSH_KEYTYPE_ED25519, 0, &key) != SSH_OK) m->why = "The key couldn't be made";