feat: add ssh server (sshd)
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
PROG_INC := ../../include
|
||||
LIBDIR := ../../lib
|
||||
LINK_LD := ../../link.ld
|
||||
OBJDIR := obj
|
||||
TARGET := ../../bin/os/sshd.elf
|
||||
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -ffreestanding \
|
||||
-fno-stack-protector -fno-stack-check -fno-rtti -fno-exceptions \
|
||||
-ffunction-sections -fdata-sections -mno-80387 -mno-mmx -mno-sse -mno-sse2 \
|
||||
-MMD -MP -I $(PROG_INC) -I $(LIBDIR)/bearssl/inc -isystem $(PROG_INC)/libc
|
||||
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
|
||||
LIBS := $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a
|
||||
.PHONY: all clean
|
||||
all: $(TARGET)
|
||||
$(TARGET): $(OBJDIR)/main.o $(LIBS) $(LINK_LD) Makefile
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJDIR)/main.o $(LIBS) -o $@
|
||||
$(OBJDIR)/main.o: main.cpp crypto.hpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c main.cpp -o $@
|
||||
-include $(OBJDIR)/main.d
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,365 @@
|
||||
#pragma once
|
||||
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/user.h>
|
||||
|
||||
#include <bearssl_block.h>
|
||||
#include <bearssl_hash.h>
|
||||
#include <bearssl_hmac.h>
|
||||
#include <bearssl_rand.h>
|
||||
#include <bearssl_rsa.h>
|
||||
|
||||
namespace sshcrypto {
|
||||
|
||||
static inline uint32_t be32(const uint8_t* data) {
|
||||
return ((uint32_t)data[0] << 24) |
|
||||
((uint32_t)data[1] << 16) |
|
||||
((uint32_t)data[2] << 8) |
|
||||
data[3];
|
||||
}
|
||||
|
||||
static inline void put32(uint8_t* data, uint32_t value) {
|
||||
data[0] = value >> 24;
|
||||
data[1] = value >> 16;
|
||||
data[2] = value >> 8;
|
||||
data[3] = value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Wipe key material. Written through a volatile pointer so the optimiser
|
||||
* cannot drop the store as dead when the buffer dies with the frame.
|
||||
*/
|
||||
static inline void zeroize(void* data, int size) {
|
||||
volatile uint8_t* p = (volatile uint8_t*)data;
|
||||
for (int i = 0; i < size; i++) p[i] = 0;
|
||||
}
|
||||
|
||||
static inline void sha256(const void* data, int size, uint8_t output[32]) {
|
||||
br_sha256_context context;
|
||||
br_sha256_init(&context);
|
||||
br_sha256_update(&context, data, size);
|
||||
br_sha256_out(&context, output);
|
||||
}
|
||||
|
||||
struct Rng {
|
||||
br_hmac_drbg_context context;
|
||||
|
||||
bool init() {
|
||||
uint8_t seed[64];
|
||||
int64_t received = montauk::getrandom(seed, sizeof(seed));
|
||||
if (received != (int64_t)sizeof(seed)) {
|
||||
montauk::memset(seed, 0, sizeof(seed));
|
||||
return false;
|
||||
}
|
||||
|
||||
br_hmac_drbg_init(&context, &br_sha256_vtable, seed, sizeof(seed));
|
||||
montauk::memset(seed, 0, sizeof(seed));
|
||||
return true;
|
||||
}
|
||||
|
||||
void bytes(void* output, int size) {
|
||||
br_hmac_drbg_generate(&context, output, size);
|
||||
}
|
||||
};
|
||||
|
||||
static inline void hex(const uint8_t* input, int size, char* output) {
|
||||
static const char* digits = "0123456789abcdef";
|
||||
for (int i = 0; i < size; i++) {
|
||||
output[i * 2] = digits[input[i] >> 4];
|
||||
output[i * 2 + 1] = digits[input[i] & 15];
|
||||
}
|
||||
output[size * 2] = 0;
|
||||
}
|
||||
|
||||
static inline int unhex(const char* input, uint8_t* output, int capacity) {
|
||||
int size = 0;
|
||||
while (input && input[0] && input[1] && size < capacity) {
|
||||
int high = montauk::user::hex_char_val(input[0]);
|
||||
int low = montauk::user::hex_char_val(input[1]);
|
||||
if (high < 0 || low < 0) break;
|
||||
output[size++] = (high << 4) | low;
|
||||
input += 2;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/*
|
||||
* The host key lives in its own config file rather than in ssh.toml so that
|
||||
* the unprivileged Settings applet, which rewrites ssh.toml whenever the
|
||||
* per-user allow table changes, never reads or round-trips private key
|
||||
* material. Note this is separation, not protection: the filesystem has no
|
||||
* permission model yet, so any local process can still read the file. Real
|
||||
* confidentiality needs FS-level access control.
|
||||
*/
|
||||
static const char HOSTKEY_CONFIG[] = "ssh_hostkey";
|
||||
static const char HOSTKEY_LEGACY_CONFIG[] = "ssh";
|
||||
|
||||
struct HostKey {
|
||||
uint8_t privateBuffer[BR_RSA_KBUF_PRIV_SIZE(2048)];
|
||||
uint8_t publicBuffer[BR_RSA_KBUF_PUB_SIZE(2048)];
|
||||
br_rsa_private_key sk = {};
|
||||
br_rsa_public_key pk = {};
|
||||
|
||||
/*
|
||||
* Earlier builds kept the key in ssh.toml. Move any such key into its own
|
||||
* file on first start so an upgrade does not change the host key (which
|
||||
* would trip every client's known-hosts check).
|
||||
*/
|
||||
bool load() {
|
||||
if (load_from(HOSTKEY_CONFIG)) return true;
|
||||
if (!load_from(HOSTKEY_LEGACY_CONFIG)) return false;
|
||||
migrate_from_legacy();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load_from(const char* configName) {
|
||||
auto config = montauk::config::load(configName);
|
||||
const char* modulus = config.get_string("host_key.n", "");
|
||||
const char* exponent = config.get_string("host_key.e", "");
|
||||
const char* primeP = config.get_string("host_key.p", "");
|
||||
const char* primeQ = config.get_string("host_key.q", "");
|
||||
const char* exponentP = config.get_string("host_key.dp", "");
|
||||
const char* exponentQ = config.get_string("host_key.dq", "");
|
||||
const char* coefficient = config.get_string("host_key.iq", "");
|
||||
|
||||
int offset = 0;
|
||||
pk.n = publicBuffer;
|
||||
pk.nlen = unhex(modulus, publicBuffer, sizeof(publicBuffer));
|
||||
pk.e = publicBuffer + pk.nlen;
|
||||
pk.elen = unhex(exponent, publicBuffer + pk.nlen,
|
||||
sizeof(publicBuffer) - pk.nlen);
|
||||
|
||||
sk.p = privateBuffer + offset;
|
||||
sk.plen = unhex(primeP, sk.p, sizeof(privateBuffer) - offset);
|
||||
offset += sk.plen;
|
||||
sk.q = privateBuffer + offset;
|
||||
sk.qlen = unhex(primeQ, sk.q, sizeof(privateBuffer) - offset);
|
||||
offset += sk.qlen;
|
||||
sk.dp = privateBuffer + offset;
|
||||
sk.dplen = unhex(exponentP, sk.dp, sizeof(privateBuffer) - offset);
|
||||
offset += sk.dplen;
|
||||
sk.dq = privateBuffer + offset;
|
||||
sk.dqlen = unhex(exponentQ, sk.dq, sizeof(privateBuffer) - offset);
|
||||
offset += sk.dqlen;
|
||||
sk.iq = privateBuffer + offset;
|
||||
sk.iqlen = unhex(coefficient, sk.iq, sizeof(privateBuffer) - offset);
|
||||
sk.n_bitlen = (uint32_t)config.get_int("host_key.bits", 2048);
|
||||
config.destroy();
|
||||
|
||||
return pk.nlen >= 256 && pk.elen > 0 &&
|
||||
sk.plen > 0 && sk.qlen > 0 &&
|
||||
sk.dplen > 0 && sk.dqlen > 0 && sk.iqlen > 0;
|
||||
}
|
||||
|
||||
/* Rewrite the in-memory key to its own file and strip it from ssh.toml. */
|
||||
void migrate_from_legacy() {
|
||||
if (save_to(HOSTKEY_CONFIG) < 0) return;
|
||||
|
||||
auto legacy = montauk::config::load(HOSTKEY_LEGACY_CONFIG);
|
||||
static const char* const parts[] = {
|
||||
"host_key.n", "host_key.e", "host_key.p", "host_key.q",
|
||||
"host_key.dp", "host_key.dq", "host_key.iq", "host_key.bits",
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof(parts) / sizeof(parts[0]); i++) {
|
||||
montauk::config::unset(&legacy, parts[i]);
|
||||
}
|
||||
montauk::config::save(HOSTKEY_LEGACY_CONFIG, &legacy);
|
||||
legacy.destroy();
|
||||
}
|
||||
|
||||
int save_to(const char* configName) {
|
||||
auto config = montauk::config::load(configName);
|
||||
char encoded[520];
|
||||
|
||||
#define SAVE_PART(key, data, size) \
|
||||
do { \
|
||||
hex((const uint8_t*)(data), (int)(size), encoded); \
|
||||
montauk::config::set_string(&config, "host_key." key, encoded); \
|
||||
} while (0)
|
||||
|
||||
SAVE_PART("n", pk.n, pk.nlen);
|
||||
SAVE_PART("e", pk.e, pk.elen);
|
||||
SAVE_PART("p", sk.p, sk.plen);
|
||||
SAVE_PART("q", sk.q, sk.qlen);
|
||||
SAVE_PART("dp", sk.dp, sk.dplen);
|
||||
SAVE_PART("dq", sk.dq, sk.dqlen);
|
||||
SAVE_PART("iq", sk.iq, sk.iqlen);
|
||||
|
||||
#undef SAVE_PART
|
||||
|
||||
montauk::config::set_int(&config, "host_key.bits", sk.n_bitlen);
|
||||
int result = montauk::config::save(configName, &config);
|
||||
config.destroy();
|
||||
montauk::memset(encoded, 0, sizeof(encoded));
|
||||
return result;
|
||||
}
|
||||
|
||||
bool generate(Rng& rng) {
|
||||
if (!br_rsa_i31_keygen((const br_prng_class**)&rng.context,
|
||||
&sk, privateBuffer,
|
||||
&pk, publicBuffer,
|
||||
2048, 65537)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return save_to(HOSTKEY_CONFIG) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/* RFC 3526 MODP group 14, represented as little-endian 32-bit limbs. */
|
||||
static constexpr int NL = 64;
|
||||
static const uint32_t P[NL] = {
|
||||
0xFFFFFFFF, 0xFFFFFFFF, 0x8AACAA68, 0x15728E5A,
|
||||
0x98FA0510, 0x15D22618, 0xEA956AE5, 0x3995497C,
|
||||
0x95581718, 0xDE2BCBF6, 0x6F4C52C9, 0xB5C55DF0,
|
||||
0xEC07A28F, 0x9B2783A2, 0x180E8603, 0xE39E772C,
|
||||
0x2E36CE3B, 0x32905E46, 0xCA18217C, 0xF1746C08,
|
||||
0x4ABC9804, 0x670C354E, 0x7096966D, 0x9ED52907,
|
||||
0x208552BB, 0x1C62F356, 0xDCA3AD96, 0x83655D23,
|
||||
0xFD24CF5F, 0x69163FA8, 0x1C55D39A, 0x98DA4836,
|
||||
0xA163BF05, 0xC2007CB8, 0xECE45B3D, 0x49286651,
|
||||
0x7C4B1FE6, 0xAE9F2411, 0x5A899FA5, 0xEE386BFB,
|
||||
0xF406B7ED, 0x0BFF5CB6, 0xA637ED6B, 0xF44C42E9,
|
||||
0x625E7EC6, 0xE485B576, 0x6D51C245, 0x4FE1356D,
|
||||
0xF25F1437, 0x302B0A6D, 0xCD3A431B, 0xEF9519B3,
|
||||
0x8E3404DD, 0x514A0879, 0x3B139B22, 0x020BBEA6,
|
||||
0x8A67CC74, 0x29024E08, 0x80DC1CD1, 0xC4C6628B,
|
||||
0x2168C234, 0xC90FDAA2, 0xFFFFFFFF, 0xFFFFFFFF,
|
||||
};
|
||||
|
||||
static inline int cmp(const uint32_t* left, const uint32_t* right) {
|
||||
for (int i = NL - 1; i >= 0; i--) {
|
||||
if (left[i] != right[i]) return left[i] > right[i] ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void subp(uint32_t* value) {
|
||||
uint64_t borrow = 0;
|
||||
for (int i = 0; i < NL; i++) {
|
||||
uint64_t difference = (uint64_t)value[i] - P[i] - borrow;
|
||||
value[i] = (uint32_t)difference;
|
||||
borrow = (difference >> 63) & 1;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void dbl(uint32_t* value) {
|
||||
uint64_t carry = 0;
|
||||
for (int i = 0; i < NL; i++) {
|
||||
uint64_t doubled = ((uint64_t)value[i] << 1) | carry;
|
||||
value[i] = doubled;
|
||||
carry = doubled >> 32;
|
||||
}
|
||||
if (carry || cmp(value, P) >= 0) subp(value);
|
||||
}
|
||||
|
||||
static inline void montmul(const uint32_t* left, const uint32_t* right,
|
||||
uint32_t* output) {
|
||||
uint32_t temporary[NL + 1] = {};
|
||||
|
||||
/* P[0] is -1, therefore -P^-1 mod 2^32 is 1. */
|
||||
for (int i = 0; i < NL; i++) {
|
||||
uint64_t carry = 0;
|
||||
for (int j = 0; j < NL; j++) {
|
||||
uint64_t product = (uint64_t)left[j] * right[i] +
|
||||
temporary[j] + carry;
|
||||
temporary[j] = product;
|
||||
carry = product >> 32;
|
||||
}
|
||||
|
||||
uint64_t sum = (uint64_t)temporary[NL] + carry;
|
||||
temporary[NL] = (uint32_t)sum;
|
||||
|
||||
uint32_t multiplier = temporary[0];
|
||||
carry = 0;
|
||||
for (int j = 0; j < NL; j++) {
|
||||
sum = (uint64_t)multiplier * P[j] + temporary[j] + carry;
|
||||
if (j > 0) temporary[j - 1] = (uint32_t)sum;
|
||||
carry = sum >> 32;
|
||||
}
|
||||
|
||||
sum = (uint64_t)temporary[NL] + carry;
|
||||
temporary[NL - 1] = (uint32_t)sum;
|
||||
temporary[NL] = (uint32_t)(sum >> 32);
|
||||
}
|
||||
|
||||
for (int i = 0; i < NL; i++) output[i] = temporary[i];
|
||||
if (temporary[NL] || cmp(output, P) >= 0) subp(output);
|
||||
}
|
||||
|
||||
static inline void from_be(const uint8_t* input, int size, uint32_t* output) {
|
||||
montauk::memset(output, 0, NL * 4);
|
||||
for (int i = 0; i < size && i < 256; i++) {
|
||||
output[i / 4] |= (uint32_t)input[size - 1 - i] << ((i & 3) * 8);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void to_be(const uint32_t* input, uint8_t output[256]) {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
output[255 - i] = (uint8_t)(input[i / 4] >> ((i & 3) * 8));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void modexp(const uint32_t* base, const uint8_t* exponent,
|
||||
int exponentSize, uint32_t* output) {
|
||||
uint32_t one[NL] = {};
|
||||
uint32_t rSquared[NL];
|
||||
uint32_t result[NL];
|
||||
uint32_t montgomeryBase[NL];
|
||||
uint32_t temporary[NL];
|
||||
|
||||
one[0] = 1;
|
||||
for (int i = 0; i < NL * 32; i++) dbl(one);
|
||||
for (int i = 0; i < NL; i++) rSquared[i] = one[i];
|
||||
for (int i = 0; i < NL * 32; i++) dbl(rSquared);
|
||||
for (int i = 0; i < NL; i++) result[i] = one[i];
|
||||
montmul(base, rSquared, montgomeryBase);
|
||||
|
||||
for (int i = 0; i < exponentSize; i++) {
|
||||
for (int bit = 7; bit >= 0; bit--) {
|
||||
montmul(result, result, temporary);
|
||||
for (int j = 0; j < NL; j++) result[j] = temporary[j];
|
||||
if ((exponent[i] >> bit) & 1) {
|
||||
montmul(result, montgomeryBase, temporary);
|
||||
for (int j = 0; j < NL; j++) result[j] = temporary[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t normal[NL] = {};
|
||||
normal[0] = 1;
|
||||
montmul(result, normal, output);
|
||||
}
|
||||
|
||||
struct Cipher {
|
||||
br_aes_ct_ctr_keys aes;
|
||||
uint8_t iv[12];
|
||||
uint32_t counter = 0;
|
||||
uint8_t mac[32];
|
||||
|
||||
void init(const uint8_t* key, const uint8_t* initializationVector,
|
||||
const uint8_t* macKey) {
|
||||
br_aes_ct_ctr_init(&aes, key, 16);
|
||||
montauk::memcpy(iv, initializationVector, sizeof(iv));
|
||||
counter = be32(initializationVector + 12);
|
||||
montauk::memcpy(mac, macKey, sizeof(mac));
|
||||
}
|
||||
|
||||
void crypt(void* data, int size) {
|
||||
counter = br_aes_ct_ctr_run(&aes, iv, counter, data, size);
|
||||
}
|
||||
};
|
||||
|
||||
static inline void hmac(const uint8_t key[32], const void* data, int size,
|
||||
uint8_t output[32]) {
|
||||
br_hmac_key_context keyContext;
|
||||
br_hmac_context context;
|
||||
br_hmac_key_init(&keyContext, &br_sha256_vtable, key, 32);
|
||||
br_hmac_init(&context, &keyContext, 32);
|
||||
br_hmac_update(&context, data, size);
|
||||
br_hmac_out(&context, output);
|
||||
}
|
||||
|
||||
} // namespace sshcrypto
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -Wno-unused-parameter \
|
||||
-ffreestanding -fno-stack-protector -fno-stack-check -fno-rtti -fno-exceptions \
|
||||
-ffunction-sections -fdata-sections -msse -msse2 -MMD -MP -I $(PROG_INC) \
|
||||
-I $(LIBDIR)/bearssl/inc -isystem $(PROG_INC)/libc
|
||||
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
TARGET := $(BINDIR)/apps/sshserver/sshserver.elf
|
||||
LIBS := $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a
|
||||
.PHONY: all clean
|
||||
all: $(TARGET)
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
-include $(OBJS:.o=.d)
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS SSH Server settings applet
|
||||
*/
|
||||
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/ssh.h>
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int WIN_W = 620;
|
||||
static constexpr int WIN_H = 500;
|
||||
static constexpr int PAD = 20;
|
||||
static constexpr int FOOTER_H = 48;
|
||||
static constexpr int USER_ROW_H = 38;
|
||||
static constexpr int VISIBLE_USERS = 7;
|
||||
|
||||
static WsWindow g_win;
|
||||
static Color g_accent = colors::ACCENT;
|
||||
static int g_mouse_x = -1;
|
||||
static int g_mouse_y = -1;
|
||||
static bool g_is_admin = false;
|
||||
static bool g_enabled = false;
|
||||
static bool g_saved_enabled = false;
|
||||
static bool g_dirty = false;
|
||||
static montauk::user::UserInfo g_users[montauk::user::MAX_USERS];
|
||||
static bool g_allowed[montauk::user::MAX_USERS];
|
||||
static bool g_saved_allowed[montauk::user::MAX_USERS];
|
||||
static int g_user_count = 0;
|
||||
static int g_user_scroll = 0;
|
||||
static char g_status[128] = {};
|
||||
|
||||
static Rect service_row() {
|
||||
return {PAD, 76, WIN_W - PAD * 2, 32};
|
||||
}
|
||||
|
||||
static Rect user_row(int visibleIndex) {
|
||||
return {PAD, 164 + visibleIndex * USER_ROW_H,
|
||||
WIN_W - PAD * 2, USER_ROW_H};
|
||||
}
|
||||
|
||||
static Rect apply_button() {
|
||||
return {WIN_W - PAD - 92, WIN_H - FOOTER_H + 8, 92, 31};
|
||||
}
|
||||
|
||||
static Rect revert_button() {
|
||||
Rect apply = apply_button();
|
||||
return {apply.x - 104, apply.y, 92, apply.h};
|
||||
}
|
||||
|
||||
static mtk::Theme app_theme() {
|
||||
return mtk::make_theme(g_accent);
|
||||
}
|
||||
|
||||
static void load_state() {
|
||||
char currentUser[32] = {};
|
||||
montauk::getuser(currentUser, sizeof(currentUser));
|
||||
|
||||
auto init = montauk::config::load("init");
|
||||
g_enabled = init.get_bool("services.ssh.enabled", false);
|
||||
init.destroy();
|
||||
|
||||
auto ssh = montauk::config::load("ssh");
|
||||
g_user_count = montauk::user::load_users(
|
||||
g_users, montauk::user::MAX_USERS);
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_allowed[i] = montauk::ssh::user_allowed(ssh, g_users[i].username);
|
||||
}
|
||||
ssh.destroy();
|
||||
|
||||
/* Reuse the table just loaded rather than parsing the user file twice. */
|
||||
g_is_admin = false;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
if (montauk::streq(g_users[i].username, currentUser)) {
|
||||
g_is_admin = montauk::streq(g_users[i].role, "admin");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_status[0] = 0;
|
||||
|
||||
g_saved_enabled = g_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_saved_allowed[i] = g_allowed[i];
|
||||
}
|
||||
g_dirty = false;
|
||||
}
|
||||
|
||||
static void recompute_dirty() {
|
||||
g_dirty = g_enabled != g_saved_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
if (g_allowed[i] != g_saved_allowed[i]) g_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool save_state() {
|
||||
if (!g_is_admin) {
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
"Administrator access is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto init = montauk::config::load("init");
|
||||
/*
|
||||
* init discovers services by enumerating [services.<id>] tables and needs
|
||||
* a path to launch. On a system whose init.toml predates the SSH service,
|
||||
* writing only the enabled flag would produce a key init cannot act on, so
|
||||
* write the whole service definition and let set_* overwrite in place where
|
||||
* it already exists.
|
||||
*/
|
||||
if (!init.get_string("services.ssh.path", nullptr)) {
|
||||
montauk::config::set_string(&init, "services.ssh.path",
|
||||
"0:/os/sshd.elf");
|
||||
montauk::config::set_string(&init, "services.ssh.name", "SSH server");
|
||||
montauk::config::set_bool(&init, "services.ssh.wait", false);
|
||||
montauk::config::set_bool(&init, "services.ssh.optional", true);
|
||||
}
|
||||
montauk::config::set_bool(&init, "services.ssh.enabled", g_enabled);
|
||||
int initResult = montauk::config::save("init", &init);
|
||||
init.destroy();
|
||||
|
||||
auto ssh = montauk::config::load("ssh");
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
char key[64];
|
||||
montauk::ssh::allow_key(key, sizeof(key), g_users[i].username);
|
||||
montauk::config::set_bool(&ssh, key, g_allowed[i]);
|
||||
}
|
||||
int sshResult = montauk::config::save("ssh", &ssh);
|
||||
ssh.destroy();
|
||||
|
||||
if (initResult < 0 || sshResult < 0) {
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
"Could not save SSH configuration");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_saved_enabled = g_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_saved_allowed[i] = g_allowed[i];
|
||||
}
|
||||
g_dirty = false;
|
||||
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
g_enabled ? "Saved; SSH starts on the next boot"
|
||||
: "Saved; SSH is disabled on the next boot");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void render() {
|
||||
mtk::StandaloneHost host(&g_win);
|
||||
Canvas canvas = host.canvas();
|
||||
mtk::Theme theme = app_theme();
|
||||
canvas.fill(theme.window_bg);
|
||||
|
||||
canvas.text(PAD, 20, "SSH Server", theme.text);
|
||||
canvas.text(PAD, 44,
|
||||
"Secure remote access to the MontaukOS shell",
|
||||
theme.text_subtle);
|
||||
|
||||
Rect service = service_row();
|
||||
mtk::draw_checkbox(canvas, service,
|
||||
"Start the SSH server at boot",
|
||||
mtk::check_state(g_enabled), theme,
|
||||
g_is_admin,
|
||||
service.contains(g_mouse_x, g_mouse_y));
|
||||
canvas.text(PAD + 24, 112,
|
||||
"Listens on TCP port 22 and uses MontaukOS account passwords.",
|
||||
theme.text_subtle);
|
||||
canvas.text(PAD, 142, "USERS ALLOWED TO CONNECT", theme.text_muted);
|
||||
|
||||
for (int row = 0; row < VISIBLE_USERS; row++) {
|
||||
int userIndex = g_user_scroll + row;
|
||||
if (userIndex >= g_user_count) break;
|
||||
|
||||
Rect bounds = user_row(row);
|
||||
if (userIndex & 1) {
|
||||
canvas.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h,
|
||||
mtk::mix(theme.window_bg, theme.surface, 55));
|
||||
}
|
||||
|
||||
char label[112];
|
||||
const char* displayName = g_users[userIndex].display_name[0]
|
||||
? g_users[userIndex].display_name
|
||||
: g_users[userIndex].username;
|
||||
snprintf(label, sizeof(label), "%s (%s)",
|
||||
displayName, g_users[userIndex].username);
|
||||
mtk::draw_checkbox(canvas, bounds, label,
|
||||
mtk::check_state(g_allowed[userIndex]),
|
||||
theme, g_is_admin,
|
||||
bounds.contains(g_mouse_x, g_mouse_y));
|
||||
|
||||
if (montauk::streq(g_users[userIndex].role, "admin")) {
|
||||
const char* role = "Administrator";
|
||||
canvas.text(bounds.x + bounds.w - text_width(role) - 10,
|
||||
bounds.y + 10, role, theme.text_muted);
|
||||
}
|
||||
}
|
||||
|
||||
/* Without an indicator there is nothing to show the list continues. */
|
||||
if (g_user_count > VISIBLE_USERS) {
|
||||
Rect track = {WIN_W - PAD + 4, user_row(0).y, 4,
|
||||
VISIBLE_USERS * USER_ROW_H};
|
||||
canvas.fill_rect(track.x, track.y, track.w, track.h,
|
||||
mtk::mix(theme.window_bg, theme.surface, 120));
|
||||
|
||||
int thumbHeight = track.h * VISIBLE_USERS / g_user_count;
|
||||
if (thumbHeight < 20) thumbHeight = 20;
|
||||
int span = g_user_count - VISIBLE_USERS;
|
||||
int thumbY = track.y +
|
||||
(track.h - thumbHeight) * g_user_scroll / (span ? span : 1);
|
||||
canvas.fill_rect(track.x, thumbY, track.w, thumbHeight,
|
||||
theme.text_muted);
|
||||
}
|
||||
|
||||
if (!g_is_admin) {
|
||||
canvas.text(PAD, WIN_H - FOOTER_H - 27,
|
||||
"Only administrators can change system SSH settings.",
|
||||
theme.danger);
|
||||
}
|
||||
|
||||
Rect footer = {0, WIN_H - FOOTER_H, WIN_W, FOOTER_H};
|
||||
canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface);
|
||||
mtk::draw_separator(canvas, 0, footer.y, WIN_W, theme);
|
||||
const char* footerStatus = g_status[0]
|
||||
? g_status
|
||||
: (g_dirty ? "Unsaved SSH changes" : "SSH configuration ready");
|
||||
canvas.text(PAD, footer.y + 16, footerStatus, theme.text_subtle);
|
||||
|
||||
Rect revert = revert_button();
|
||||
Rect apply = apply_button();
|
||||
mtk::draw_button(
|
||||
canvas, revert, "Revert", mtk::BUTTON_SECONDARY,
|
||||
mtk::widget_state(false, revert.contains(g_mouse_x, g_mouse_y),
|
||||
g_dirty && g_is_admin),
|
||||
theme);
|
||||
mtk::draw_button(
|
||||
canvas, apply, "Apply", mtk::BUTTON_PRIMARY,
|
||||
mtk::widget_state(false, apply.contains(g_mouse_x, g_mouse_y),
|
||||
g_dirty && g_is_admin),
|
||||
theme);
|
||||
|
||||
host.present();
|
||||
}
|
||||
|
||||
static void handle_mouse(const montauk::abi::WinEvent& event) {
|
||||
g_mouse_x = event.mouse.x;
|
||||
g_mouse_y = event.mouse.y;
|
||||
|
||||
bool pressed = (event.mouse.buttons & 1) &&
|
||||
!(event.mouse.prev_buttons & 1);
|
||||
|
||||
if (event.mouse.scroll) {
|
||||
g_user_scroll += event.mouse.scroll > 0 ? -1 : 1;
|
||||
int maximum = g_user_count > VISIBLE_USERS
|
||||
? g_user_count - VISIBLE_USERS
|
||||
: 0;
|
||||
if (g_user_scroll < 0) g_user_scroll = 0;
|
||||
if (g_user_scroll > maximum) g_user_scroll = maximum;
|
||||
}
|
||||
|
||||
if (!pressed || !g_is_admin) return;
|
||||
|
||||
if (service_row().contains(g_mouse_x, g_mouse_y)) {
|
||||
g_enabled = !g_enabled;
|
||||
/* A stale "Saved" message would otherwise hide the unsaved state. */
|
||||
g_status[0] = 0;
|
||||
} else {
|
||||
for (int row = 0; row < VISIBLE_USERS; row++) {
|
||||
int userIndex = g_user_scroll + row;
|
||||
if (userIndex < g_user_count &&
|
||||
user_row(row).contains(g_mouse_x, g_mouse_y)) {
|
||||
g_allowed[userIndex] = !g_allowed[userIndex];
|
||||
g_status[0] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (revert_button().contains(g_mouse_x, g_mouse_y) && g_dirty) {
|
||||
load_state();
|
||||
} else if (apply_button().contains(g_mouse_x, g_mouse_y) && g_dirty) {
|
||||
save_state();
|
||||
}
|
||||
recompute_dirty();
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init()) montauk::exit(1);
|
||||
|
||||
g_accent = mtk::load_system_accent();
|
||||
load_state();
|
||||
if (!g_win.create("SSH Server", WIN_W, WIN_H)) montauk::exit(1);
|
||||
render();
|
||||
|
||||
while (g_win.id >= 0 && !g_win.closed) {
|
||||
montauk::abi::WinEvent event;
|
||||
int count = g_win.poll(&event);
|
||||
if (count <= 0) {
|
||||
montauk::yield();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type == 1) {
|
||||
handle_mouse(event);
|
||||
} else if (event.type == 2) {
|
||||
g_win.width = event.resize.w;
|
||||
g_win.height = event.resize.h;
|
||||
} else if (event.type == 3) {
|
||||
g_win.closed = true;
|
||||
} else if (event.type == 0 && event.key.pressed &&
|
||||
event.key.scancode == 1) {
|
||||
g_win.closed = true;
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[app]
|
||||
id = "sshserver"
|
||||
name = "SSH Server"
|
||||
binary = "sshserver.elf"
|
||||
icon = "utilities-terminal.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = false
|
||||
|
||||
[desktop]
|
||||
section = "settings"
|
||||
admin_only = false
|
||||
@@ -0,0 +1,2 @@
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
Reference in New Issue
Block a user