Files
bluessh/ssh.lua
T
2026-08-09 13:54:08 +03:00

264 lines
8.4 KiB
Lua

local program_name = "BlueSSH"
local program_version = "1.0"
local program_string = program_name .. "_" .. program_version
local component = require("component")
local internet = component.internet
local event = require("event")
local computer = require("computer")
local serialize = require("serialize")
local function lazy_require(name)
local this = {}
local module
setmetatable(this, {
__index = function(_, key)
module = module or require(name)
return module[key]
end,
__call = function(_, ...)
module = module or require(name)
return module(...)
end
})
return this
end
local curve25519 = lazy_require("curve25519")
local function parse_ssh_version(str)
local proto, software, comments = str:match("^SSH%-(%d+%.%d+)%-(%S+)%s?(.*)\r\n$")
if not proto then return nil, "invalid SSH version string" end
return proto, software, comments or nil
end
local function encode_ssh_version(proto, software, comments)
return "SSH-" .. proto .. "-" .. software .. (comments and (" " .. comments) or "") .. "\r\n"
end
local random
if not component.isAvailable("data") or component.data.random == nil then
terminal.write("ssh: WARNING: THIS PROGRAM IS NOT CRYPTOGRAPHICALLY SAFE WITHOUT A TIER 2 DATA CARD, DO NOT CONNECT TO A REAL SERVER WITH THIS\n")
random = function(n)
local parts = {}
for i = 1, n do parts[i] = math.random(0, 255) end
return table.concat(parts)
end
else
random = component.data.random
end
local SSH_MSG_DISCONNECT = 1
local SSH_MSG_IGNORE = 2
local SSH_MSG_UNIMPLEMENTED = 3
local SSH_MSG_DEBUG = 4
local SSH_MSG_SERVICE_REQUEST = 5
local SSH_MSG_SERVICE_ACCEPT = 6
local SSH_MSG_KEXINIT = 20
local SSH_MSG_NEWKEYS = 21
local function socket()
local this = {}
local _sock
local _host
local _port
function this.connect(host, port)
_host = host; _port = port
_sock = internet.connect(host, port)
local ok = false
for _ = 1, 50 do
local err
ok, err = _sock.finishConnect()
if ok == nil then
return nil, err
end
if ok == true then
return true
end
coroutine.yield()
end
return nil, "timed out"
end
local _read_buf = ""
function this.read(n)
while #_read_buf < n do
local chunk, err = _sock.read(n - #_read_buf)
if chunk == nil then return nil, err end
_read_buf = _read_buf .. chunk
end
local to_return = _read_buf:sub(1, n);
_read_buf = _read_buf:sub(n + 1, -1)
return to_return
end
function this.read_until_crlf(n)
local chunk, err = _sock.read(n)
if not chunk then return nil, err end
_read_buf = _read_buf .. chunk
local crlf_pos = _read_buf:find("\r\n", 1, true)
if crlf_pos == nil then return nil end
local to_return = _read_buf:sub(1, crlf_pos + 1)
_read_buf = _read_buf:sub(crlf_pos + 2, -1)
return to_return
end
function this.read_uint8()
local x, err = this.read(1)
if x == nil then return nil, err end
return string.unpack(">I1", x)
end
function this.read_uint32()
local x, err = this.read(4)
if x == nil then return nil, err end
return string.unpack(">I4", x)
end
function this.read_packet(mac_length)
mac_length = mac_length or 0
local packet_length, err = this.read_uint32()
if not packet_length then return nil, err end
local padding_length, err = this.read_uint8()
if not padding_length then return nil, err end
local payload, err = this.read(packet_length - padding_length - 1)
if not payload then return nil, err end
local random_padding, err = this.read(padding_length)
if not random_padding then return nil, err end
local mac, err = this.read(mac_length)
if not mac then return nil, err end
return payload
end
function this.write_packet(payload, block_size)
block_size = block_size or 8
local min_padding = 4
local padding_length = block_size - ((5 + #payload) % block_size)
if padding_length < min_padding then
padding_length = padding_length + block_size
end
local padding = random(padding_length)
local packet_length = 1 + #payload + padding_length
local packet = string.pack(">I4", packet_length)
.. string.char(padding_length)
.. payload
.. padding
return this.write(packet)
end
local function parse_namelist(payload, pos)
local len = string.unpack(">I4", payload, pos)
local s = payload:sub(pos + 4, pos + 4 + len - 1)
local list = {}
if #s > 0 then
for name in s:gmatch("[^,]+") do
list[#list + 1] = name
end
end
return list, pos + 4 + len
end
local function encode_namelist(list)
return string.pack(">s4", table.concat(list or {}, ","))
end
function this.parse_kexinit(payload)
local packet_type = payload:byte(1)
if packet_type ~= SSH_MSG_KEXINIT then return nil, "unknown server side error" end
local pos = 2
local kex = {}
kex.cookie = payload:sub(pos, pos + 15)
pos = pos + 16
kex.kex_algorithms, pos = parse_namelist(payload, pos)
kex.server_host_key_algorithms, pos = parse_namelist(payload, pos)
kex.encryption_algorithms_client_to_server, pos = parse_namelist(payload, pos)
kex.encryption_algorithms_server_to_client, pos = parse_namelist(payload, pos)
kex.mac_algorithms_client_to_server, pos = parse_namelist(payload, pos)
kex.mac_algorithms_server_to_client, pos = parse_namelist(payload, pos)
kex.compression_algorithms_client_to_server, pos = parse_namelist(payload, pos)
kex.compression_algorithms_server_to_client, pos = parse_namelist(payload, pos)
kex.languages_client_to_server, pos = parse_namelist(payload, pos)
kex.languages_server_to_client, pos = parse_namelist(payload, pos)
kex.first_kex_packet_follows = payload:byte(pos) ~= 0
pos = pos + 1
-- next 4 bytes are reserved (uint32 == 0)
return kex
end
function this.build_kexinit(algs)
return string.char(SSH_MSG_KEXINIT) ..
random(16) ..
encode_namelist(algs.kex_algorithms) ..
encode_namelist(algs.server_host_key_algorithms) ..
encode_namelist(algs.encryption_algorithms_client_to_server) ..
encode_namelist(algs.encryption_algorithms_server_to_client) ..
encode_namelist(algs.mac_algorithms_client_to_server) ..
encode_namelist(algs.mac_algorithms_server_to_client) ..
encode_namelist(algs.compression_algorithms_client_to_server) ..
encode_namelist(algs.compression_algorithms_server_to_client) ..
encode_namelist(algs.languages_client_to_server) ..
encode_namelist(algs.languages_server_to_client) ..
'\0' .. -- first_kex_packet_follows
'\0\0\0\0' -- reserved
end
function this.write(x)
local written = 0
while written < #x do
local bytes, err = _sock.write(x:sub(written + 1, -1))
if not bytes then return nil, err end
written = written + bytes
end
return written
end
return this
end
local sock = socket()
sock.connect("localhost", 22)
-- RFC 4253: The maximum length of the string is 255 characters
local server_protoversion, server_softwareversion, server_comments = parse_ssh_version(sock.read_until_crlf(255))
if server_protoversion ~= "2.0" then
return
end
sock.write(encode_ssh_version("2.0", program_string))
print(sock.parse_kexinit(sock.read_packet()))
--[[
local my_algs = {
kex_algorithms = {},
server_host_key_algorithms = {},
encryption_algorithms_client_to_server = {},
encryption_algorithms_server_to_client = {},
mac_algorithms_client_to_server = {},
mac_algorithms_server_to_client = {},
compression_algorithms_client_to_server = { "none" },
compression_algorithms_server_to_client = { "none" },
languages_client_to_server = {},
languages_server_to_client = {},
}
local my_kexinit_payload = sock.build_kexinit(my_algs)
sock.write_packet(my_kexinit_payload)
]]