1308 lines
43 KiB
Plaintext
1308 lines
43 KiB
Plaintext
#- Simple SSH server in Berry by Christian Baars
|
|
# includes rudimentary terminal and SFTP server
|
|
# this is demo code and not intended for production use
|
|
# written from scratch, no libraries used
|
|
# DO NOT OPEN A GH ISSUE, but feel free to use GH discussions
|
|
# needs some crypto primitives:
|
|
#define USE_BERRY_CRYPTO_EC_C25519
|
|
#define USE_BERRY_CRYPTO_CHACHA_POLY
|
|
#define USE_BERRY_CRYPTO_ED25519
|
|
-#
|
|
|
|
class SSH_MSG
|
|
static DISCONNECT = 1
|
|
static IGNORE = 2
|
|
static SERVICE_REQUEST = 5
|
|
static SERVICE_ACCEPT = 6
|
|
static KEXINIT = 20
|
|
static NEWKEYS = 21
|
|
static KEXDH_INIT = 30
|
|
static KEX_ECDH_REPLY = 31
|
|
static USERAUTH_REQUEST = 50
|
|
static USERAUTH_FAILURE = 51
|
|
static USERAUTH_SUCCESS = 52
|
|
static USERAUTH_BANNER = 53
|
|
static GLOBAL_REQUEST = 80
|
|
static REQUEST_SUCCESS = 81
|
|
static REQUEST_FAILURE = 82
|
|
static CHANNEL_OPEN = 90
|
|
static CHANNEL_OPEN_CONFIRMATION = 91
|
|
static CHANNEL_OPEN_FAILURE = 92
|
|
static CHANNEL_WINDOW_ADJUST = 93
|
|
static CHANNEL_DATA = 94
|
|
static CHANNEL_EXTENDED_DATA = 95
|
|
static CHANNEL_EOF = 96
|
|
static CHANNEL_CLOSE = 97
|
|
static CHANNEL_REQUEST = 98
|
|
static CHANNEL_SUCCESS = 99
|
|
static CHANNEL_FAILURE =100
|
|
|
|
static def get_name_list(buffer, index, length)
|
|
import string
|
|
if length == 0 || length > (size(buffer) - 5)
|
|
return nil
|
|
end
|
|
var names = buffer[index + 4 .. index + 3 + length]
|
|
return string.split(names.asstring(),",")
|
|
end
|
|
|
|
static def get_string(buffer, index, length)
|
|
import string
|
|
if length == 0 || length > (size(buffer) - 5)
|
|
return nil
|
|
end
|
|
var name = buffer[index + 4 .. index + 3 + length]
|
|
return name.asstring()
|
|
end
|
|
|
|
static def get_bytes(buffer, index, length)
|
|
import string
|
|
if length == 0 || length > (size(buffer) - 5)
|
|
return nil
|
|
end
|
|
var b = buffer[index + 4 .. index + 3 + length]
|
|
return b
|
|
end
|
|
|
|
static def get_item_length(buf)
|
|
return buf.geti(0,-4)
|
|
end
|
|
|
|
static def add_string(buf, str_entry)
|
|
buf.add(size(str_entry),-4)
|
|
buf .. str_entry
|
|
end
|
|
|
|
static def add_mpint(buf, entry)
|
|
if entry[0] & 128 != 0
|
|
entry = bytes("00") + entry
|
|
end
|
|
buf.add(size(entry),-4)
|
|
buf .. entry
|
|
end
|
|
|
|
static def make_mpint(buf)
|
|
var mpint = bytes(size(buf) + 5)
|
|
if buf[0] & 128 != 0
|
|
buf = bytes("00") + buf
|
|
end
|
|
mpint.add(size(buf),-4)
|
|
mpint .. buf
|
|
return mpint
|
|
end
|
|
end
|
|
|
|
class TERMINAL
|
|
var in_buf, session
|
|
|
|
def init(session)
|
|
self.session = session
|
|
self.in_buf = bytes(64)
|
|
end
|
|
|
|
def process(data)
|
|
self.in_buf .. data
|
|
if data == bytes("0d")
|
|
var c = self.in_buf.asstring()
|
|
var r = tasmota.cmd(f"{c}")
|
|
self.in_buf.clear()
|
|
if r
|
|
return "\r\n" + r.tostring() + "\r\n> "
|
|
else
|
|
return "\r\n>"
|
|
end
|
|
else
|
|
return data.asstring()
|
|
end
|
|
return ""
|
|
end
|
|
end
|
|
|
|
class PATH # helper class to hold the current directory
|
|
var p # path components in a list
|
|
|
|
def init()
|
|
import string
|
|
self.p = []
|
|
end
|
|
|
|
def set(p)
|
|
import string
|
|
import path
|
|
|
|
if path.isdir(p) != true
|
|
return false
|
|
end
|
|
|
|
var new = string.split(p,"/")
|
|
self.p = []
|
|
for c:new
|
|
if c != ""
|
|
self.p.push(c)
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
def dir_up()
|
|
if size(self.p) > 0
|
|
self.p.pop()
|
|
end
|
|
end
|
|
|
|
def get_url()
|
|
var url = "/"
|
|
for c:self.p
|
|
if c != ""
|
|
url += f"{c}/"
|
|
end
|
|
end
|
|
return url
|
|
end
|
|
end
|
|
|
|
class SFTP_FILE
|
|
var url, file, length, written, is_writing, is_reading, id
|
|
var append_flag, chunk_limit
|
|
|
|
#define SSH_FXF_READ 0x00000001
|
|
#define SSH_FXF_WRITE 0x00000002
|
|
#define SSH_FXF_APPEND 0x00000004
|
|
#define SSH_FXF_CREAT 0x00000008
|
|
#define SSH_FXF_TRUNC 0x00000010
|
|
#define SSH_FXF_EXCL 0x00000020
|
|
|
|
def init(url, pflags)
|
|
import path
|
|
if path.exists(url) != true
|
|
if pflags&1 == false && pflags&4 == false
|
|
return nil
|
|
end
|
|
end
|
|
if pflags&1
|
|
self.file = open(url,"r")
|
|
log(f"SFTP: open file for read {url}",4)
|
|
end
|
|
if pflags&2
|
|
self.file = open(url,"w")
|
|
log(f"SFTP: open file for write {log}",4)
|
|
end
|
|
if pflags&4
|
|
self.append_flag = true
|
|
log(f"SFTP: open file for append {log}",4)
|
|
else
|
|
self.append_flag = false
|
|
end
|
|
self.url = url
|
|
self.is_writing = false
|
|
self.chunk_limit = 4096
|
|
end
|
|
|
|
def deinit()
|
|
self.close()
|
|
end
|
|
|
|
def write(data, offset, id)
|
|
log(f"SFTP: write file {data} at position {offset}",3)
|
|
if self.append_flag == false
|
|
self.file.seek(offset)
|
|
end
|
|
|
|
self.length = data.geti(0,-4)
|
|
log(f"SFTP: file length {self.length}", 3)
|
|
|
|
self.id = id
|
|
self.written = size(data) - 4
|
|
if self.written < self.length
|
|
self.is_writing = true
|
|
end
|
|
return self.file.write(data[4..]) # skip length
|
|
end
|
|
|
|
def append(data)
|
|
if self.file
|
|
self.written += size(data)
|
|
if self.written == self.length
|
|
self.is_writing = false
|
|
end
|
|
return self.file.write(data)
|
|
end
|
|
end
|
|
|
|
def read(len, offset, id)
|
|
self.file.seek(offset)
|
|
if len > self.chunk_limit # stay below 4096 max packet size in the end
|
|
len = self.chunk_limit
|
|
end
|
|
if self.file
|
|
var b = self.file.readbytes(len)
|
|
return b
|
|
end
|
|
return nil
|
|
end
|
|
|
|
def close()
|
|
log(f"SFTP: close file {self.url}",3)
|
|
if self.file
|
|
self.file.close()
|
|
end
|
|
end
|
|
end
|
|
|
|
class SFTP
|
|
static INIT = 1
|
|
static VERSION = 2
|
|
static OPEN = 3
|
|
static CLOSE = 4
|
|
static READ = 5
|
|
static WRITE = 6
|
|
static LSTAT = 7
|
|
static FSETSTAT = 10
|
|
static OPENDIR = 11
|
|
static READDIR = 12
|
|
static REALPATH = 16
|
|
static STAT = 17
|
|
static STATUS = 101
|
|
static DATA = 103
|
|
static NAME = 104
|
|
static ATTRS = 105
|
|
|
|
var session, dir_list, dir, file
|
|
|
|
def init(session)
|
|
self.session = session
|
|
self.dir = PATH()
|
|
log("SFTP started .. very incomplete!",1)
|
|
end
|
|
|
|
def fsize(url)
|
|
import path
|
|
if path.exists(url) == true && path.isdir(url) == false
|
|
var f = open(url,"r")
|
|
var sz = f.size()
|
|
f.close()
|
|
return sz
|
|
end
|
|
return 0
|
|
end
|
|
|
|
def fdate(url)
|
|
import path
|
|
if path.exists(url) == true
|
|
return path.last_modified(url)
|
|
end
|
|
return 0
|
|
end
|
|
|
|
def long_name(url)
|
|
var date = self.fdate(url)
|
|
var sz = self.fsize(url)
|
|
var m = tasmota.strftime("%B", date)[0..2]
|
|
var dt = tasmota.strftime("%d %H:%M", date)
|
|
var pre = "-"
|
|
if sz ==0 pre = "d" end # TODO: really check if dir
|
|
return f"{pre}rwxrwxr-x 1 admin all {sz:8i} {m} {dt} {url}"
|
|
end
|
|
|
|
def read_dir(url, id)
|
|
if size(self.dir_list) == 0
|
|
return self.status(id, 1) # EOF
|
|
end
|
|
var r = bytes("00000000") # size
|
|
r .. SFTP.NAME
|
|
r .. id
|
|
r.add(size(self.dir_list),-4) # count
|
|
for i:self.dir_list
|
|
SSH_MSG.add_string(r,i)
|
|
SSH_MSG.add_string(r,self.long_name(i))
|
|
r .. self.attribs(i) # file attributes
|
|
end
|
|
r.seti(0,size(r)-4,-4)
|
|
self.dir_list = []
|
|
return r
|
|
end
|
|
|
|
def attribs(url)
|
|
import path
|
|
var date = self.fdate(url)
|
|
var sz = self.fsize(url)
|
|
var perms = 777
|
|
var a = bytes("0800000f") # flags for extended size|uid|perm|time
|
|
a.add(0, -4) # high bytes of size
|
|
a.add(sz,-4) # is uint64
|
|
a.add(0,-4) # uid - superuser
|
|
a.add(0,-4) # gid - superuser
|
|
if path.isdir(url)
|
|
a.add(perms|40000, -4) # permissions for dir
|
|
else
|
|
a.add(perms|100000, -4) # permissions for file
|
|
end
|
|
a.add(date,-4)
|
|
a.add(date,-4)
|
|
return a
|
|
end
|
|
|
|
def status(id,code)
|
|
var s = bytes("0000000065") # packet type SSH_FXP_STATUS 101
|
|
s .. id
|
|
s.add(code,-4)
|
|
s .. bytes(-8) # two empty strings
|
|
s.seti(0,size(s)-4,-4)
|
|
log(f"SFTP: status {code} for {id}",4)
|
|
return s
|
|
end
|
|
|
|
def handle(id,url)
|
|
var h = bytes("0000000066") # packet type SSH_FXP_HANDLE 102
|
|
h .. id
|
|
SSH_MSG.add_string(h,url)
|
|
h.seti(0,size(h)-4,-4)
|
|
return h
|
|
end
|
|
|
|
def stat_for_url(id, url)
|
|
import path
|
|
if path.exists(url)
|
|
var r = bytes("00000000") # size
|
|
r .. SFTP.ATTRS
|
|
r..id
|
|
r .. self.attribs(url) # file attributes
|
|
r.seti(0,size(r)-4,-4)
|
|
return r
|
|
end
|
|
return self.status(id, 2) # NO_SUCH_FILE
|
|
end
|
|
|
|
def open_file(id,url,pflags,attr)
|
|
self.file = SFTP_FILE(url,pflags)
|
|
if self.file
|
|
return self.handle(id,url)
|
|
end
|
|
return self.status(id, 2) # NO_SUCH_FILE
|
|
end
|
|
|
|
def path_name(url,id)
|
|
var r = bytes("00000000") # size
|
|
r .. SFTP.NAME
|
|
r .. id
|
|
r.add(1,-4) # count
|
|
SSH_MSG.add_string(r,url)
|
|
SSH_MSG.add_string(r,"")
|
|
r .. self.attribs(url) # file attributes
|
|
r.seti(0,size(r)-4,-4)
|
|
return r
|
|
end
|
|
|
|
def process(d)
|
|
log(f"SFTP: process SFTP __________________________",3)
|
|
var r = bytes()
|
|
var unfinished = true
|
|
var ptype, id
|
|
if self.file
|
|
log(f"SFTP: file is open {self.file.url} {self.file.written} {self.file.length} {self.file.is_writing}",4)
|
|
if self.file.is_writing == true
|
|
log(f"SFTP: append {d}",3)
|
|
self.file.append(d)
|
|
if self.file.is_writing == false
|
|
return self.status(self.file.id, 0) # SSH_FX_OK
|
|
end
|
|
return "" # will resolve later to MSG_IGNORE
|
|
end
|
|
end
|
|
if self.file
|
|
var cmds = size(d)/32
|
|
if cmds == 0
|
|
cmds = 1
|
|
end
|
|
self.file.chunk_limit = 4096/cmds # read command 32 bytes
|
|
log(f"SSH: multiple commands: {cmds}",3)
|
|
end
|
|
while unfinished == true
|
|
ptype = d[4] # https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-3
|
|
id = d[5..8]
|
|
log(f"SFTP: type {ptype}, id {id}, data {d}", 3)
|
|
if ptype == SFTP.INIT
|
|
r = bytes('000000050200000003') # no extended data support, ver 3
|
|
elif ptype == SFTP.LSTAT
|
|
var url = d[13..].asstring()
|
|
log(f"SFTP LSTAT for: {url}",3)
|
|
r = self.stat_for_url(id,url)
|
|
elif ptype == SFTP.STAT
|
|
var url = d[13..].asstring()
|
|
log(f"SFTP STAT for: {url}",3)
|
|
r = self.stat_for_url(id,url)
|
|
elif ptype == SFTP.OPEN
|
|
var next_index = 9
|
|
var next_length = SSH_MSG.get_item_length(d[next_index..])
|
|
var url = SSH_MSG.get_string(d, next_index, next_length)
|
|
next_index += next_length + 4
|
|
var pflags = d.geti(next_index,-4)
|
|
next_index += 4
|
|
var attr = d[next_index..]
|
|
log(f"SFTP OPEN: {url} with {pflags} and {attr}",3)
|
|
r = self.open_file(id,url,pflags,attr)
|
|
elif ptype == SFTP.READ
|
|
var next_index = 9
|
|
var next_length = SSH_MSG.get_item_length(d[next_index..])
|
|
var url = SSH_MSG.get_string(d, next_index, next_length)
|
|
next_index += next_length + 8
|
|
var offset = d.geti(next_index,-4) # uint64
|
|
next_index += 4
|
|
var len = d.geti(next_index,-4) # uint32
|
|
next_index += 4
|
|
log(f"SFTP READ: {url} - {len} bytes from {offset}",3)
|
|
var fbytes = self.file.read(len,offset,id)
|
|
if size(fbytes) == 0
|
|
r = self.status(id, 1) # FX_EOF 1
|
|
else
|
|
var _r = bytes("00000000") # size
|
|
_r .. SFTP.DATA
|
|
_r .. id
|
|
SSH_MSG.add_string(_r, fbytes)
|
|
_r.seti(0,size(_r)-4,-4)
|
|
r .. _r
|
|
end
|
|
if next_index < size(d) - 9
|
|
unfinished = true
|
|
d = d[next_index..]
|
|
continue
|
|
end
|
|
elif ptype == SFTP.WRITE
|
|
var next_index = 9
|
|
var next_length = SSH_MSG.get_item_length(d[next_index..])
|
|
var url = SSH_MSG.get_string(d, next_index, next_length)
|
|
next_index += next_length + 8
|
|
var offset = d.geti(next_index,-4) # uint64
|
|
next_index += 4
|
|
var data = d[next_index..]
|
|
log(f"SFTP WRITE: {url}",3)
|
|
self.file.write(data,offset, id) # Todo: check success
|
|
if self.file.is_writing == false
|
|
r = self.status(self.file.id, 0) # SSH_FX_OK
|
|
else
|
|
r = "" # -> MSG_IGNORE
|
|
end
|
|
elif ptype == SFTP.OPENDIR
|
|
var url = d[13..].asstring()
|
|
if url == ""
|
|
url = "/"
|
|
end
|
|
log(f"SFTP OPENDIR: {url}",3)
|
|
if self.dir.set(url)
|
|
import path
|
|
self.dir_list = path.listdir(self.dir.get_url())
|
|
r = self.handle(id,url)
|
|
else
|
|
r = self.status(id, 2) # NO_SUCH_FILE
|
|
end
|
|
elif ptype == SFTP.READDIR
|
|
var url = d[13..].asstring()
|
|
log(f"SFTP READDIR: {url}",3)
|
|
r = self.read_dir(url,id)
|
|
elif ptype == SFTP.CLOSE
|
|
log("SFTP CLOSE",3)
|
|
r = self.status(id, 0) # SSH_FX_OK
|
|
self.file = nil
|
|
elif ptype == SFTP.REALPATH
|
|
log("SFTP REALPATH",3)
|
|
var url = d[13..].asstring()
|
|
if url == "."
|
|
url = "/"
|
|
end
|
|
r = self.path_name(url,id)
|
|
elif ptype == SFTP.FSETSTAT
|
|
log("SFTP FSETSTAT",3)
|
|
#ignore for now
|
|
self.file.close()
|
|
r = self.status(id, 0) # SSH_FX_OK
|
|
else
|
|
log(f"SFTP: unknown packet type {ptype}", 2)
|
|
r = self.status(id,8) #OP_UNSUPPORTED
|
|
end
|
|
unfinished = false
|
|
end
|
|
return r
|
|
end
|
|
end
|
|
|
|
class BIN_PACKET
|
|
var packet_length, padding_length, payload, payload_length, padding, mac, mac_length
|
|
var expected_length
|
|
var complete, session, encrypted, buf, overrun_buf
|
|
|
|
def init(buf, session, encrypted)
|
|
self.session = session
|
|
self.packet_length = buf.geti(0,-4)
|
|
self.expected_length = self.packet_length + 4
|
|
if encrypted == true
|
|
self.packet_length = self.get_length(buf)
|
|
log(f"SSH: new bin_packet with {self.packet_length} bytes",3)
|
|
self.expected_length = self.packet_length + 4 + 16 # mac
|
|
end
|
|
if self.expected_length > 32768
|
|
log(f"SSH: Unusual high packet length {self.expected_length} - assume decoding error!!",1)
|
|
self.expected_length = size(buf)
|
|
self.packet_length = size(buf) - 20
|
|
end
|
|
self.buf = bytes(self.expected_length)
|
|
self.encrypted = encrypted
|
|
self.append(buf)
|
|
end
|
|
|
|
def get_length(packet)
|
|
import crypto
|
|
var c = crypto.CHACHA20_POLY1305()
|
|
var length = packet[0..3]
|
|
var iv = bytes(-12)
|
|
iv.seti(8,self.session.seq_nr_rx,-4)
|
|
c.chacha_run(self.session.KEY_C_S_header,iv,0,length)
|
|
return length.geti(0,-4)
|
|
end
|
|
|
|
def check_packet()
|
|
import crypto
|
|
var c = crypto.CHACHA20_POLY1305()
|
|
var iv = bytes(-12)
|
|
iv.seti(8,self.session.seq_nr_rx,-4)
|
|
var data = self.buf[0.. self.packet_length + 3]
|
|
var poly_key = bytes(-32)
|
|
c.chacha_run(self.session.KEY_C_S_main, iv, 0 ,poly_key)
|
|
var given_mac = self.buf[self.packet_length+4..self.packet_length+19]
|
|
var mac = c.poly_run(data,poly_key)
|
|
if mac != given_mac
|
|
#TODO: disconect
|
|
log(f"SSH: MAC MISMATCH!! {mac} - {given_mac} ", 1)
|
|
end
|
|
end
|
|
|
|
def decrypt()
|
|
import crypto
|
|
var c = crypto.CHACHA20_POLY1305()
|
|
var iv = bytes(-12)
|
|
iv.seti(8,self.session.seq_nr_rx,-4)
|
|
var data = self.buf[4..-17]
|
|
c.chacha_run(self.session.KEY_C_S_main, iv, 1, data)
|
|
self.buf.setbytes(4,data)
|
|
# print(self.buf, size(self.buf))
|
|
return
|
|
end
|
|
|
|
def decode()
|
|
self.padding_length = self.buf[4]
|
|
self.payload_length = self.packet_length - self.padding_length - 1
|
|
# print(self.packet_length, self.padding_length, self.payload_length)
|
|
self.payload = self.buf[5 .. 5 + self.payload_length - 1]
|
|
self.padding = self.buf[5 + self.payload_length .. 5 + self.payload_length + self.padding_length - 1]
|
|
# print(self.payload)
|
|
end
|
|
|
|
def append(buf)
|
|
self.buf .. buf
|
|
if size(self.buf) > self.expected_length
|
|
log(f"must split TCP packet:{self.expected_length} _ {size(self.buf) - self.expected_length} ",4)
|
|
self.session.overrun_buf = self.buf[self.expected_length ..]
|
|
end
|
|
if size(self.buf) >= self.expected_length
|
|
log(f"SSH: got complete packet: {self.expected_length} _ {size(self.buf)}",4)
|
|
self.complete = true
|
|
if self.encrypted == true
|
|
self.check_packet()
|
|
self.decrypt()
|
|
end
|
|
self.decode()
|
|
else
|
|
self.complete = false
|
|
end
|
|
end
|
|
|
|
def encrypt(packet)
|
|
# print(packet)
|
|
import crypto
|
|
var c = crypto.CHACHA20_POLY1305()
|
|
var iv = bytes(-12)
|
|
iv.seti(8,self.session.seq_nr_tx,-4)
|
|
var length = packet[0..3]
|
|
c.chacha_run(self.session.KEY_S_C_header,iv,0,length)
|
|
var data = packet[4..]
|
|
c.chacha_run(self.session.KEY_S_C_main, iv, 1, data)
|
|
var enc_packet = length + data
|
|
|
|
var poly_key = bytes(-32)
|
|
c.chacha_run(self.session.KEY_S_C_main,iv,0,poly_key)
|
|
var mac = c.poly_run(enc_packet,poly_key)
|
|
|
|
return enc_packet + mac
|
|
end
|
|
|
|
def create(payload, encrypted)
|
|
import crypto
|
|
var paylength = size(payload)
|
|
var padlength = 8-((5 + paylength)%8)
|
|
if encrypted == true
|
|
padlength -= 4
|
|
end
|
|
if padlength < 5
|
|
padlength += 16
|
|
end
|
|
var padding = crypto.random(padlength)
|
|
var bin = bytes(256)
|
|
bin.add(1 + paylength + padlength, -4)
|
|
bin .. padlength
|
|
bin .. payload
|
|
bin .. padding
|
|
if encrypted == true
|
|
return self.encrypt(bin)
|
|
end
|
|
return bin
|
|
end
|
|
end
|
|
|
|
|
|
class HANDSHAKE
|
|
var state, bin_packet, session
|
|
|
|
var V_C # client's identification string (CR and LF excluded)
|
|
static V_S = "SSH-2.0-TasmotaSSH_0.1" # server's identification string (CR and LF excluded)
|
|
var I_C # payload of the client's SSH_MSG_KEXINIT
|
|
var I_S # payload of the server's SSH_MSG_KEXINIT
|
|
var K_S # server's public host key
|
|
var Q_C # client's ephemeral public key octet string
|
|
var Q_S # server's ephemeral public key octet string
|
|
var K # shared secret
|
|
|
|
var H # hash of above
|
|
|
|
var host_key # server's secret host key bytes
|
|
|
|
|
|
def init(session)
|
|
self.state = 0
|
|
self.create_host_keys()
|
|
self.session = session
|
|
end
|
|
|
|
def create_host_keys()
|
|
import crypto
|
|
var ed = crypto.ED25519()
|
|
var example_seed = bytes("a60c6c7107be5da01ba7f7bc6a08e1d0faa27e1db9327514823fdac5f8e750dd") # could be any crypto.random(32)
|
|
self.host_key = ed.secret_key(example_seed) #bytes("a60c6c7107be5da01ba7f7bc6a08e1d0faa27e1db9327514823fdac5f8e750dd")
|
|
var pk = bytes(64)
|
|
SSH_MSG.add_string(pk, "ssh-ed25519")
|
|
SSH_MSG.add_string(pk,self.host_key[-32..]) # public key is simply the last 32 bytes of the secret key
|
|
self.K_S = pk
|
|
end
|
|
|
|
def kexinit_to_client()
|
|
import crypto
|
|
var cookie = crypto.random(16)
|
|
var kex_algorithms = "curve25519-sha256,kex-strict-s-v00@openssh.com,kex-strict-s"
|
|
var server_host_key_algorithms = "ssh-ed25519"
|
|
var encryption_algorithms_client_to_server = "chacha20-poly1305@openssh.com"
|
|
var encryption_algorithms_server_to_client = "chacha20-poly1305@openssh.com"
|
|
var mac_algorithms_client_to_server = ""
|
|
var mac_algorithms_server_to_client = ""
|
|
var compression_algorithms_client_to_server = "none"
|
|
var compression_algorithms_server_to_client = "none"
|
|
var languages_client_to_server = ""
|
|
var languages_server_to_client = ""
|
|
|
|
var payload = bytes(256)
|
|
payload .. SSH_MSG.KEXINIT
|
|
payload .. cookie
|
|
SSH_MSG.add_string(payload,kex_algorithms)
|
|
SSH_MSG.add_string(payload,server_host_key_algorithms)
|
|
SSH_MSG.add_string(payload,encryption_algorithms_client_to_server)
|
|
SSH_MSG.add_string(payload,encryption_algorithms_server_to_client)
|
|
SSH_MSG.add_string(payload,mac_algorithms_client_to_server)
|
|
SSH_MSG.add_string(payload,mac_algorithms_server_to_client)
|
|
SSH_MSG.add_string(payload,compression_algorithms_client_to_server)
|
|
SSH_MSG.add_string(payload,compression_algorithms_client_to_server)
|
|
SSH_MSG.add_string(payload,languages_client_to_server)
|
|
SSH_MSG.add_string(payload,languages_server_to_client)
|
|
payload .. 0 # false - first_kex_follows
|
|
payload.add(0,-4) # reserved
|
|
self.I_S = payload.copy()
|
|
return self.bin_packet.create(payload)
|
|
end
|
|
|
|
def create_KEX_ECDH_REPLY()
|
|
import crypto
|
|
var hash = bytes(2048)
|
|
SSH_MSG.add_string(hash, self.V_C)
|
|
SSH_MSG.add_string(hash, self.V_S)
|
|
SSH_MSG.add_string(hash, self.I_C)
|
|
SSH_MSG.add_string(hash, self.I_S)
|
|
SSH_MSG.add_string(hash, self.K_S)
|
|
SSH_MSG.add_string(hash, self.Q_C)
|
|
SSH_MSG.add_string(hash, self.Q_S)
|
|
SSH_MSG.add_mpint(hash, self.K)
|
|
|
|
var sha256 = crypto.SHA256()
|
|
sha256.update(hash)
|
|
self.H = sha256.out()
|
|
|
|
var eddsa25519 = crypto.ED25519()
|
|
var SIG = eddsa25519.sign(self.H,self.host_key)
|
|
# print(SIG)
|
|
|
|
var payload = bytes(256)
|
|
payload .. SSH_MSG.KEX_ECDH_REPLY
|
|
# print(self.K_S, size(self.K_S), self.Q_S, size(self.Q_S), H, size(H) )
|
|
SSH_MSG.add_string(payload, self.K_S)
|
|
SSH_MSG.add_string(payload, self.Q_S)
|
|
var HS = bytes(128)
|
|
SSH_MSG.add_string(HS, "ssh-ed25519")
|
|
SSH_MSG.add_string(HS,SIG)
|
|
SSH_MSG.add_string(payload, HS)
|
|
return self.bin_packet.create(payload)
|
|
end
|
|
|
|
def create_ephemeral(payload)
|
|
log("SSH: create ephemeral keys",3)
|
|
import crypto
|
|
var ephem_key = crypto.random(32)
|
|
self.Q_S = (crypto.EC_C25519().public_key(ephem_key))
|
|
self.Q_C = payload[5..]
|
|
self.K = (crypto.EC_C25519().shared_key(ephem_key, self.Q_C))
|
|
# print(ephem_key self.Q_S, self.K)
|
|
return self.create_KEX_ECDH_REPLY()
|
|
end
|
|
|
|
def kexinit_from_client() # mainly logging function
|
|
import string
|
|
var buf = self.bin_packet.payload
|
|
var k = {}
|
|
log(f"cookie: {buf[1..16].tohex()}",3)
|
|
var next_index = 17
|
|
var next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"kex_algorithms: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
for i:SSH_MSG.get_name_list(buf, next_index, next_length)
|
|
if string.find(i, "kex-strict-c") >= 0 self.session.strict_mode = true end
|
|
end
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"server_host_key_algorithms: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"encryption_algorithms_client_to_server: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"encryption_algorithms_server_to_client: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"mac_algorithms_client_to_server: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"mac_algorithms_server_to_client: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"compression_algorithms_client_to_server: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"compression_algorithms_server_to_client: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"languages_client_to_server: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
log(f"languages_server_to_client: {SSH_MSG.get_name_list(buf, next_index, next_length)}",3)
|
|
next_index += next_length + 4
|
|
log(f"first_kex_packet_follows: {buf[next_index]}",3)
|
|
end
|
|
|
|
def send_NEWKEYS()
|
|
log("SSH: send new keys",2)
|
|
var payload = bytes(-1)
|
|
payload[0] = SSH_MSG.NEWKEYS
|
|
self.session.prepare(self.K,self.H)
|
|
return self.bin_packet.create(payload)
|
|
end
|
|
|
|
def process(buf)
|
|
var response = bytes()
|
|
if self.state == 0
|
|
self.state = 1
|
|
self.V_C = buf[0..-3].asstring() # strip LF
|
|
return f"{self.V_S}\r\n"
|
|
elif self.state == 1
|
|
if self.bin_packet
|
|
self.bin_packet.append(buf)
|
|
else
|
|
self.bin_packet = BIN_PACKET(buf,self.session, false)
|
|
end
|
|
if self.bin_packet.complete == true
|
|
if self.bin_packet.payload[0] == SSH_MSG.KEXINIT
|
|
self.I_C = self.bin_packet.payload.copy()
|
|
self.kexinit_from_client()
|
|
response = self.kexinit_to_client()
|
|
elif self.bin_packet.payload[0] == SSH_MSG.KEXDH_INIT
|
|
response = self.create_ephemeral(self.bin_packet.payload)
|
|
elif self.bin_packet.payload[0] == SSH_MSG.NEWKEYS
|
|
response = self.send_NEWKEYS()
|
|
self.state = 2
|
|
elif self.bin_packet.payload[0] == SSH_MSG.DISCONNECT
|
|
log("SSH: client did disconnect",1)
|
|
else
|
|
log("SSH: unknown packet type: {self.bin_packet.payload[0]}", 1)
|
|
end
|
|
self.bin_packet = nil
|
|
end
|
|
return response
|
|
elif self.state == 2
|
|
|
|
end
|
|
log("SSH: unknown packet",1)
|
|
return ""
|
|
end
|
|
end
|
|
|
|
class SESSION
|
|
var up, strict_mode, client_pub_key
|
|
var H, K, ID
|
|
var bin_packet
|
|
var KEY_C_S_main, KEY_S_C_main, KEY_C_S_header, KEY_S_C_header
|
|
var seq_nr_rx, seq_nr_tx, channel_nr
|
|
var send_queue, overrun_buf
|
|
var type # terminal or SFTP
|
|
|
|
static MAX_PACKET_SIZE = 4096 # we must process the whole packet (crypt, auth, etc)
|
|
|
|
static user = "admin"
|
|
static password = "1234"
|
|
|
|
static banner = " / \\ Secure Wireless Serial Interface\n"
|
|
"/ /|\\ \\ SSH Terminal Server on %s\n"
|
|
" \\_/ Copyright (C) 2025 Tasmota %s\n"
|
|
|
|
def init()
|
|
self.up = false
|
|
self.seq_nr_rx = -2 # very unsure about this!!!
|
|
self.seq_nr_tx = -1
|
|
self.send_queue = []
|
|
self.strict_mode = false # support by client
|
|
end
|
|
|
|
def deinit()
|
|
self.type = nil
|
|
self.bin_packet = nil
|
|
end
|
|
|
|
def send_banner()
|
|
var r = bytes()
|
|
r .. SSH_MSG.USERAUTH_BANNER
|
|
var s2 = tasmota.cmd("status 2")["StatusFWR"]
|
|
var hw = s2["Hardware"]
|
|
var vs = s2["Version"]
|
|
var strict_mode = ""
|
|
if self.strict_mode == false
|
|
strict_mode = "\n\r WARNING: outdated SSH-client, connection is vulnerable to Terrapin!!!\r\n"
|
|
end
|
|
SSH_MSG.add_string(r,format(self.banner,hw,vs) + strict_mode)
|
|
SSH_MSG.add_string(r,"") # language
|
|
var p = BIN_PACKET(bytes(-32),self,false)
|
|
self.overrun_buf = nil
|
|
return p.create(r ,true)
|
|
end
|
|
|
|
def check_pub_key()
|
|
import persist
|
|
var r = bytes(32)
|
|
if persist.known_hosts == nil
|
|
persist.known_hosts = []
|
|
end
|
|
for key:persist.known_hosts
|
|
if key == self.client_pub_key
|
|
log("SSH: known client",2)
|
|
r .. SSH_MSG.USERAUTH_SUCCESS
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
end
|
|
r .. SSH_MSG.USERAUTH_FAILURE
|
|
SSH_MSG.add_string(r,"password")
|
|
r .. 0
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
|
|
def handle_service_request()
|
|
var name = SSH_MSG.get_string(self.bin_packet.payload, 1, SSH_MSG.get_item_length(self.bin_packet.payload[1..]))
|
|
log(f"SSH: service request: {name}",2)
|
|
if name == "ssh-userauth"
|
|
var r = bytes(64)
|
|
r .. SSH_MSG.SERVICE_ACCEPT
|
|
SSH_MSG.add_string(r,name)
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
self.send_queue.push(/->self.send_banner())
|
|
return enc_r
|
|
end
|
|
var r = bytes(64)
|
|
r .. SSH_MSG.USERAUTH_SUCCESS
|
|
log(f"SSH: unhandled request {r}",1)
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
|
|
|
|
def handle_userauth_request()
|
|
var r = bytes(32)
|
|
var buf = self.bin_packet.payload
|
|
var next_index = 1
|
|
var next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var user_name = SSH_MSG.get_string(buf, next_index, next_length)
|
|
if user_name != self.user
|
|
r .. SSH_MSG.USERAUTH_FAILURE
|
|
SSH_MSG.add_string(r,"unknown user")
|
|
r .. 0
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var service_name = SSH_MSG.get_string(buf, next_index, next_length)
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var method_name = SSH_MSG.get_string(buf, next_index, next_length)
|
|
if method_name == "none"
|
|
r .. SSH_MSG.USERAUTH_FAILURE
|
|
SSH_MSG.add_string(r,"publickey,password")
|
|
r .. 0
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
next_index += next_length + 4
|
|
var bool_field = buf[next_index]
|
|
next_index += 1
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var key_algo = SSH_MSG.get_string(buf, next_index, next_length) #var name is "context sensitive"
|
|
if method_name == "password"
|
|
if key_algo != self.password
|
|
r .. SSH_MSG.USERAUTH_FAILURE
|
|
SSH_MSG.add_string(r,"wrong password")
|
|
r .. 0
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
end
|
|
next_index += next_length + 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var algo_blob = SSH_MSG.get_bytes(buf, next_index, next_length) #var name is "context sensitive"
|
|
if method_name == "publickey"
|
|
log(f"SSH: public key auth: {key_algo}",2)
|
|
self.client_pub_key = algo_blob[-32..].tohex()
|
|
return self.check_pub_key()
|
|
end
|
|
# print(user_name,service_name,method_name,bool_field,key_algo,size(algo_blob),algo_blob)
|
|
r .. SSH_MSG.USERAUTH_SUCCESS
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
|
|
def handle_channel_open()
|
|
var buf = self.bin_packet.payload
|
|
var next_index = 1
|
|
var next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var channel_type = SSH_MSG.get_string(buf, next_index, next_length)
|
|
next_index += next_length + 4
|
|
self.channel_nr = buf.geti(next_index,-4)
|
|
next_index += 4
|
|
var window_size = buf.geti(next_index,-4)
|
|
next_index += 4
|
|
var packet_size = buf.geti(next_index,-4)
|
|
log(f"SSH: type {channel_type}, nr{self.channel_nr}, window size {window_size}, packet size {packet_size}",2)
|
|
var r = bytes(64)
|
|
r .. SSH_MSG.CHANNEL_OPEN_CONFIRMATION
|
|
r.add(self.channel_nr,-4)
|
|
r.add(self.channel_nr,-4)
|
|
r.add(window_size,-4)
|
|
r.add(SESSION.MAX_PACKET_SIZE,-4)
|
|
# print(r)
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
|
|
def handle_channel_request()
|
|
var buf = self.bin_packet.payload
|
|
var next_index = 1
|
|
var channel = buf.geti(next_index,4)
|
|
next_index += 4
|
|
var next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var req_type_type = SSH_MSG.get_string(buf, next_index, next_length)
|
|
next_index += next_length + 4
|
|
var want_reply = buf[next_index]
|
|
next_index += 1
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var term = SSH_MSG.get_string(buf, next_index, next_length)
|
|
next_index += next_length + 4
|
|
var width_c = buf.geti(next_index,-4)
|
|
next_index += 4
|
|
var height_c = buf.geti(next_index,-4)
|
|
next_index += 4
|
|
var width_p = buf.geti(next_index,-4)
|
|
next_index += 4
|
|
var height_p = buf.geti(next_index,-4)
|
|
next_index += 4
|
|
next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var terminal_modes = SSH_MSG.get_string(buf, next_index, next_length)
|
|
log(f"{channel},{req_type_type},{want_reply},{term,width_c},{height_c},{width_p},{height_p}",3)
|
|
if req_type_type == "shell"
|
|
self.type = TERMINAL()
|
|
elif req_type_type == "subsystem" && term == "sftp"
|
|
self.type = SFTP()
|
|
end
|
|
var r = bytes(64)
|
|
if want_reply
|
|
r .. SSH_MSG.CHANNEL_SUCCESS # TODO: may really check
|
|
else
|
|
r .. SSH_MSG.IGNORE
|
|
end
|
|
r.add(self.channel_nr,-4)
|
|
# print(r)
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
|
|
def handle_channel_data()
|
|
var buf = self.bin_packet.payload
|
|
var next_index = 1
|
|
var channel = buf.geti(next_index,-4)
|
|
next_index += 4
|
|
var next_length = SSH_MSG.get_item_length(buf[next_index..])
|
|
var data = SSH_MSG.get_bytes(buf, next_index, next_length)
|
|
log(f"SSH: ch {channel} data {next_length} {data}",3)
|
|
var t_r = self.type.process(data)
|
|
if t_r == ""
|
|
# self.seq_nr_rx -= 1 # pending write job or something else
|
|
var r = bytes()
|
|
r .. SSH_MSG.IGNORE
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
var r = bytes()
|
|
r .. SSH_MSG.CHANNEL_DATA
|
|
r.add(self.channel_nr,-4)
|
|
SSH_MSG.add_string(r,t_r)
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
|
|
def close_channel()
|
|
var r = bytes(16)
|
|
r .. SSH_MSG.CHANNEL_CLOSE
|
|
r.add(self.channel_nr,-4)
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
|
|
def process(data)
|
|
var r = bytes()
|
|
if self.bin_packet
|
|
self.bin_packet.append(data)
|
|
else
|
|
self.bin_packet = BIN_PACKET(data, self ,true)
|
|
end
|
|
if self.bin_packet.complete == true
|
|
if self.bin_packet.payload[0] == SSH_MSG.SERVICE_REQUEST
|
|
return self.handle_service_request()
|
|
elif self.bin_packet.payload[0] == SSH_MSG.USERAUTH_REQUEST
|
|
log("USERAUTH_REQUEST",3)
|
|
return self.handle_userauth_request()
|
|
elif self.bin_packet.payload[0] == SSH_MSG.CHANNEL_OPEN
|
|
log("CHANNEL_OPEN__REQUEST",3)
|
|
return self.handle_channel_open()
|
|
elif self.bin_packet.payload[0] == SSH_MSG.CHANNEL_REQUEST
|
|
log("CHANNEL_REQUEST",3)
|
|
return self.handle_channel_request()
|
|
elif self.bin_packet.payload[0] == SSH_MSG.CHANNEL_DATA
|
|
log("CHANNEL_DATA",3)
|
|
return self.handle_channel_data()
|
|
elif self.bin_packet.payload[0] == SSH_MSG.CHANNEL_EOF
|
|
log("CHANNEL_EOF",3)
|
|
return self.close_channel()
|
|
elif self.bin_packet.payload[0] == SSH_MSG.CHANNEL_CLOSE
|
|
log("CHANNEL_CLOSE",3)
|
|
return self.close_channel()
|
|
elif self.bin_packet.payload[0] == SSH_MSG.DISCONNECT
|
|
log("SSH: client did disconnect",1)
|
|
return ""
|
|
else
|
|
log(f"SSH: unhandled session message type: {self.bin_packet.payload[0]}", 2)
|
|
end
|
|
else
|
|
self.seq_nr_rx -= 1 # TODO: check
|
|
return ""
|
|
end
|
|
r .. SSH_MSG.IGNORE
|
|
var enc_r = self.bin_packet.create(r ,true)
|
|
return enc_r
|
|
end
|
|
|
|
def generate_keys(K,H,third,id)
|
|
import crypto
|
|
var sha256 = crypto.SHA256()
|
|
sha256.update(SSH_MSG.make_mpint(K))
|
|
sha256.update(H)
|
|
if classof(third) != bytes
|
|
sha256.update(bytes().fromstring(third))
|
|
else
|
|
sha256.update(third)
|
|
end
|
|
if id != nil
|
|
sha256.update(id)
|
|
end
|
|
return sha256.out()
|
|
end
|
|
|
|
def prepare(K,H)
|
|
self.K = K
|
|
self.H = H
|
|
self.ID = H.copy()
|
|
self.KEY_C_S_main = self.generate_keys(K,H,"C",H)
|
|
self.KEY_C_S_header = self.generate_keys(K,H,self.KEY_C_S_main)
|
|
self.KEY_S_C_main = self.generate_keys(K,H,"D",H)
|
|
self.KEY_S_C_header = self.generate_keys(K,H,self.KEY_S_C_main)
|
|
log("SSH: session keys created",3)
|
|
# print(self.KEY_C_S_main, self.KEY_C_S_header, self.KEY_S_C_main, self.KEY_S_C_header)
|
|
self.up = true
|
|
if self.strict_mode == true
|
|
self.seq_nr_rx = -1 # reset to handle Terrapin attack
|
|
self.seq_nr_tx = -1
|
|
end
|
|
end
|
|
end
|
|
|
|
class SSH : Driver
|
|
|
|
var connection, server, client
|
|
var handshake, session, loop
|
|
static port = 22
|
|
|
|
def init()
|
|
self.server = tcpserver(self.port) # connection for control data
|
|
self.connection = false
|
|
tasmota.add_driver(self)
|
|
log(f"SSH: init server on port {self.port}",1)
|
|
end
|
|
|
|
def every_50ms()
|
|
if self.connection == true
|
|
self.loop()
|
|
elif self.server.hasclient()
|
|
self.client = self.server.acceptasync()
|
|
self.session = SESSION()
|
|
self.handshake = HANDSHAKE(self.session)
|
|
self.loop = /->self.run_loop()
|
|
self.connection = true
|
|
self.pubClientInfo()
|
|
else
|
|
self.handshake = nil
|
|
self.connection = false
|
|
end
|
|
end
|
|
|
|
def every_second()
|
|
if self.client && self.connection != false
|
|
if self.client.connected() == false
|
|
self.pubClientInfo()
|
|
self.connection = false
|
|
self.session = nil
|
|
self.client = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
def pubClientInfo()
|
|
import mqtt
|
|
var payload = self.client.info().tostring()
|
|
mqtt.publish("SSH",format("{'server':%s}", payload))
|
|
end
|
|
|
|
def run_loop()
|
|
if self.connection == true
|
|
self.handleConnection()
|
|
end
|
|
end
|
|
|
|
def send(packet)
|
|
if self.client.listening() == false
|
|
log("SSH: client not listening",3)
|
|
self.loop = /->self.send(packet)
|
|
return # back to Tasmota
|
|
end
|
|
var written = self.client.write(packet)
|
|
while written < size(packet)
|
|
log(f"SSH: written only {written} of {size(packet)}",1)
|
|
self.loop = /->self.send(packet[written..])
|
|
return # back to Tasmota
|
|
end
|
|
self.session.seq_nr_tx += 1
|
|
self.loop = /->self.run_loop()
|
|
end
|
|
|
|
def sendResponse(resp)
|
|
var session = self.session
|
|
var bin = session.bin_packet
|
|
session.bin_packet = nil
|
|
self.send(resp)
|
|
if size(session.send_queue) != 0
|
|
self.send(session.send_queue.pop()())
|
|
end
|
|
log(f"SSH: {self.session.seq_nr_tx} >>> {resp} _ {size(resp)} bytes",3)
|
|
end
|
|
|
|
def handleConnection() # main loop for incoming commands
|
|
var response
|
|
var d
|
|
if self.session.overrun_buf
|
|
d = self.session.overrun_buf.copy()
|
|
self.session.overrun_buf = nil
|
|
log(f"SSH: got overrun packet: {size(d)}",3)
|
|
else
|
|
d = self.client.readbytes()
|
|
end
|
|
if size(d) == 0
|
|
return
|
|
end
|
|
self.session.seq_nr_rx += 1
|
|
log(f"SSH: {self.session.seq_nr_rx} <<< {d} _ {size(d)} bytes",3)
|
|
if self.session.up == true
|
|
response = self.session.process(d)
|
|
if response != ""
|
|
self.sendResponse(response)
|
|
end
|
|
elif self.handshake
|
|
response = self.handshake.process(d)
|
|
if size(response) != 0
|
|
self.sendResponse(response)
|
|
if size(response) > 5 && response[5] == SSH_MSG.NEWKEYS
|
|
self.handshake = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def key_save()
|
|
if self.session
|
|
if self.session.client_pub_key
|
|
import persist
|
|
if persist.known_hosts == nil
|
|
persist.known_hosts = []
|
|
end
|
|
for key:persist.known_hosts
|
|
if key == self.session.client_pub_key
|
|
tasmota.resp_cmnd_str("SSH: key already known")
|
|
return
|
|
end
|
|
end
|
|
persist.known_hosts.push(self.session.client_pub_key)
|
|
persist.save(true)
|
|
tasmota.resp_cmnd_str("SSH: key saved")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
var ssh = SSH()
|
|
|
|
tasmota.add_cmd("ssh_key_save", /->ssh.key_save())
|