commit ab75431b8a406b44dd57c70c0d214faf3534e15a Author: Yakov Shatilov Date: Tue Feb 25 16:07:22 2025 +0300 first diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aabe767 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:alpine AS builder +LABEL builder=true +WORKDIR /build + +ADD go.mod . + +COPY . . +RUN go mod download +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" -o ./cmd/app/main ./cmd/app/main.go + +FROM alpine + +WORKDIR /build + +COPY --from=builder /build/cmd/app/main /build/cmd/main + +CMD ["./cmd/main"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..63ad816 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +run: + go run ./cmd/app/main.go +update: + go get -u ./... diff --git a/cmd/app/main.go b/cmd/app/main.go new file mode 100644 index 0000000..f928dbe --- /dev/null +++ b/cmd/app/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "gommunicator/internal/communicator" + "gommunicator/internal/input" + "gommunicator/internal/logger" + "gommunicator/internal/memory" + "gommunicator/internal/ssh" + "gommunicator/internal/usecase" +) + +const key = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACC2W7jqv+IrEDb6dg2vR81uoUHnpCUueBZB0ajetr13hAAAAJjLUSTky1Ek +5AAAAAtzc2gtZWQyNTUxOQAAACC2W7jqv+IrEDb6dg2vR81uoUHnpCUueBZB0ajetr13hA +AAAEA1C1fC0k8MZ5S5vjcg43y2//ikwDvRbOp6GsLLAn/ZzrZbuOq/4isQNvp2Da9HzW6h +QeekJS54FkHRqN62vXeEAAAADnlhc2hAeWFzaC1hc3VzAQIDBAUGBw== +-----END OPENSSH PRIVATE KEY-----` + +func main() { + log := logger.SetupLogger("prod") + + mem := memory.New() + communicator := communicator.New().WithDefaultServerClient() + uc := usecase.New(log, mem, communicator) + + go input.New(uc, log).Run() + + srv, err := ssh.New(log, uc, []byte(key)) + if err != nil { + panic(err) + } + if err := srv.Start("127.0.0.1:8022"); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5bec677 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module gommunicator + +go 1.23.6 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/charmbracelet/log v0.4.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/crypto v0.34.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6622352 --- /dev/null +++ b/go.sum @@ -0,0 +1,40 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA= +golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= diff --git a/internal/communicator/communicator.go b/internal/communicator/communicator.go new file mode 100644 index 0000000..00d9376 --- /dev/null +++ b/internal/communicator/communicator.go @@ -0,0 +1,48 @@ +package communicator + +import ( + "fmt" + "log/slog" + + "gommunicator/internal/entity" +) + +type Client interface { + Send(msg string) error + Username() string +} + +type Communicator struct { + log *slog.Logger + clients []Client +} + +func New() *Communicator { + return &Communicator{ + clients: make([]Client, 0), + } +} + +func (c *Communicator) WithDefaultServerClient() *Communicator { + client := entity.NewServerClient() + c.clients = append(c.clients, client) + return c +} + +func (c *Communicator) AddClient(client Client) { + c.clients = append(c.clients, client) +} + +func (c *Communicator) SendMsg(msg, fromUsername string) { + // format + msg = fmt.Sprintf("%s > %s", fromUsername, msg) + // send + for _, client := range c.clients { + if fromUsername == client.Username() { + continue + } + if err := client.Send(msg); err != nil { + c.log.Error("failed to send message", "err", err) + } + } +} diff --git a/internal/entity/client.go b/internal/entity/client.go new file mode 100644 index 0000000..6f70f77 --- /dev/null +++ b/internal/entity/client.go @@ -0,0 +1,46 @@ +package entity + +import ( + "fmt" + + "golang.org/x/term" +) + +type ServerClient struct{} + +func NewServerClient() *ServerClient { + return &ServerClient{} +} + +func (c *ServerClient) Send(msg string) error { + fmt.Println(msg) + return nil +} + +func (c *ServerClient) Username() string { + return "server" +} + +type TerminalClient struct { + terminal *term.Terminal + username string +} + +func NewTerminalClient(term *term.Terminal, username string) *TerminalClient { + return &TerminalClient{ + terminal: term, + username: username, + } +} + +func (c *TerminalClient) Send(msg string) error { + _, err := fmt.Fprintf(c.terminal, "%s\n", msg) + if err != nil { + return err + } + return nil +} + +func (c *TerminalClient) Username() string { + return c.username +} diff --git a/internal/input/const.go b/internal/input/const.go new file mode 100644 index 0000000..11a3e1e --- /dev/null +++ b/internal/input/const.go @@ -0,0 +1,5 @@ +package input + +const helpMessage = `server commands: +/help, ? - show this message +/new-password - generate new password for clients` diff --git a/internal/input/input.go b/internal/input/input.go new file mode 100644 index 0000000..ca45e3b --- /dev/null +++ b/internal/input/input.go @@ -0,0 +1,57 @@ +package input + +import ( + "bufio" + "fmt" + "log/slog" + "os" + + "gommunicator/internal/usecase" +) + +type Input struct { + log *slog.Logger + uc *usecase.Usecase +} + +func New(uc *usecase.Usecase, log *slog.Logger) *Input { + return &Input{ + log: log, + uc: uc, + } +} + +func (i *Input) Run() { + for { + reader := bufio.NewReader(os.Stdin) + msg, err := reader.ReadString('\n') + if err != nil { + i.log.Error("failed to scan input", "err", err) + continue + } + if len(msg) < 1 { + continue + } + // observe command + if msg[0] == '/' || msg == "?" { + switch msg { + case "?", "/help": + fmt.Println(helpMessage) + case "/new-password": + pass, err := i.uc.GenNewPassword() + if err != nil { + i.log.Error("failed to generate new client's password", "err", err) + break + } + i.log.Info("new server password generated", "password", pass) + } + continue + } + + if msg[len(msg)-1] == '\n' { + msg = msg[:len(msg)-1] + } + + i.uc.SendMessage(msg, "server") + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..e5d7ed8 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,34 @@ +package logger + +import ( + "log/slog" + "os" + + prettyLogger "github.com/charmbracelet/log" +) + +const ( + envDev = "dev" + envProd = "prod" +) + +func SetupLogger(env string) *slog.Logger { + var log *slog.Logger + + switch env { + case envDev: + handler := prettyLogger.NewWithOptions(os.Stdout, prettyLogger.Options{ + Level: prettyLogger.DebugLevel, + ReportTimestamp: true, + }) + log = slog.New(handler) + case envProd: + handler := prettyLogger.NewWithOptions(os.Stdout, prettyLogger.Options{ + Level: prettyLogger.InfoLevel, + ReportTimestamp: true, + }) + log = slog.New(handler) + } + + return log +} diff --git a/internal/memory/memory.go b/internal/memory/memory.go new file mode 100644 index 0000000..3332f44 --- /dev/null +++ b/internal/memory/memory.go @@ -0,0 +1,35 @@ +package memory + +import "sync" + +type Memory struct { + passwords map[string]struct{} + mu sync.RWMutex +} + +func New() *Memory { + mem := Memory{ + passwords: make(map[string]struct{}, 16), + mu: sync.RWMutex{}, + } + return &mem +} + +func (m *Memory) SavePassword(password string) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.passwords[password] = struct{}{} + + return nil +} + +func (m *Memory) PasswordExists(password string) (bool, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if _, ok := m.passwords[password]; ok { + return true, nil + } + return false, nil +} diff --git a/internal/ssh/const.go b/internal/ssh/const.go new file mode 100644 index 0000000..81432f4 --- /dev/null +++ b/internal/ssh/const.go @@ -0,0 +1,6 @@ +package ssh + +const helpMessage = `server commands: +/help - show this message +/exit - disconnect from the server +` diff --git a/internal/ssh/password.go b/internal/ssh/password.go new file mode 100644 index 0000000..f6c40f9 --- /dev/null +++ b/internal/ssh/password.go @@ -0,0 +1,44 @@ +package ssh + +import ( + "errors" + "time" + + "golang.org/x/term" +) + +func (s *Server) newPassword() error { + password, err := s.uc.GenNewPassword() + if err != nil { + s.log.Error("failed to generate new password", "err", err) + return err + } + s.log.Info("new server password generated", "password", password) + return nil +} + +// get user password from terminal +func (s *Server) passwordRequest(term *term.Terminal) error { + // check password + attempts := 3 + for { + pass, err := term.ReadPassword("Enter your password: ") + if err != nil { + return err + } + ok, err := s.uc.PasswordExists(pass) + if err != nil { + return err + } + if ok { + term.Write([]byte("Login success\n")) + return nil + } + time.Sleep(time.Second * 1) + attempts -= 1 + if attempts <= 0 { + return errors.New("invalid password") + } + term.Write([]byte("Permission denied, please try again.\n")) + } +} diff --git a/internal/ssh/server.go b/internal/ssh/server.go new file mode 100644 index 0000000..4d86898 --- /dev/null +++ b/internal/ssh/server.go @@ -0,0 +1,154 @@ +package ssh + +import ( + "fmt" + "log/slog" + "net" + + "gommunicator/internal/entity" + "gommunicator/internal/usecase" + + "golang.org/x/crypto/ssh" + "golang.org/x/term" +) + +type Server struct { + log *slog.Logger + uc *usecase.Usecase + sshConfig *ssh.ServerConfig + sshSigner *ssh.Signer + socket *net.Listener +} + +func New(log *slog.Logger, uc *usecase.Usecase, privateKey []byte) (*Server, error) { + signer, err := ssh.ParsePrivateKey(privateKey) + if err != nil { + return nil, err + } + config := ssh.ServerConfig{ + NoClientAuth: true, + } + config.AddHostKey(signer) + + server := Server{ + sshConfig: &config, + sshSigner: &signer, + log: log, + uc: uc, + } + + return &server, nil +} + +func (s *Server) Start(laddr string) error { + // Once a ServerConfig has been configured, connections can be accepted. + socket, err := net.Listen("tcp", laddr) + if err != nil { + return err + } + s.socket = &socket + s.log.Info("Listening on", "addr", laddr) + // generate password + if err := s.newPassword(); err != nil { + return err + } + // handle connnections + for { + conn, err := socket.Accept() + if err != nil { + s.log.Info("Failed to accept connection, aborting loop", "err", err) + return err + } + // From a standard TCP connection to an encrypted SSH connection + sshConn, channels, requests, err := ssh.NewServerConn(conn, s.sshConfig) + if err != nil { + s.log.Info("Failed to handshake", "err", err) + continue + } + s.log.Info("New connection", "addr", sshConn.RemoteAddr(), "user", sshConn.User()) + + go ssh.DiscardRequests(requests) + go s.handleChannels(channels, sshConn) + } +} + +func (s *Server) handleChannels(channels <-chan ssh.NewChannel, conn *ssh.ServerConn) { + for ch := range channels { + if t := ch.ChannelType(); t != "session" { + ch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) + continue + } + + channel, requests, err := ch.Accept() + if err != nil { + continue + } + + go func(in <-chan *ssh.Request) { + defer channel.Close() + for req := range in { + // log.Println("Request: ", req.Type, string(req.Payload)) + ok := false + switch req.Type { + case "shell": + // We don't accept any commands (Payload), + // only the default shell. + if len(req.Payload) == 0 { + ok = true + } + case "pty-req": + // Responding 'ok' here will let the client + // know we have a pty ready for input + ok = true + case "window-change": + continue // no response + } + req.Reply(ok, nil) + } + }(requests) + go s.handleShell(channel, conn.User()) + } +} + +func (s *Server) handleShell(channel ssh.Channel, username string) { + defer func() { + channel.Close() + s.log.Info("User disconnected", "user", username) + }() + // create terminal + term := term.NewTerminal(channel, fmt.Sprintf("%s > ", username)) + // check password + err := s.passwordRequest(term) + if err != nil { + return + } + // add as client + s.uc.AddMessageClient(entity.NewTerminalClient(term, username)) + // Recieve user input and send to server + for { + // get user input + line, err := term.ReadLine() + if err != nil { + break + } + + if len(line) > 0 { + if string(line[0]) == "/" { + switch line { + case "/exit": + return + case "/help": + term.Write([]byte(helpMessage)) + default: + term.Write([]byte(helpMessage)) + } + continue + } + + s.log.Debug("new message", "user", username, "messg", line) + + // send message to room + s.uc.SendMessage(line, username) + } + } +} diff --git a/internal/usecase/msg.go b/internal/usecase/msg.go new file mode 100644 index 0000000..a3707cd --- /dev/null +++ b/internal/usecase/msg.go @@ -0,0 +1,11 @@ +package usecase + +import "gommunicator/internal/communicator" + +func (u *Usecase) AddMessageClient(client communicator.Client) { + u.communicator.AddClient(client) +} + +func (u *Usecase) SendMessage(msg, fromUsername string) { + u.communicator.SendMsg(msg, fromUsername) +} diff --git a/internal/usecase/password.go b/internal/usecase/password.go new file mode 100644 index 0000000..b011edc --- /dev/null +++ b/internal/usecase/password.go @@ -0,0 +1,42 @@ +package usecase + +import ( + "fmt" + "time" + + "golang.org/x/exp/rand" +) + +// GenNewPassword generates new password, writes it to the memory and returns. +func (u *Usecase) GenNewPassword() (string, error) { + rand.Seed(uint64(time.Now().UnixNano())) + // gen new password + pass := genPasswordString() + // save to the memory + if err := u.memory.SavePassword(pass); err != nil { + return "", fmt.Errorf("failed to save password in the memory: %w", err) + } + return pass, nil +} + +const ( + passwordLen = 8 + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" +) + +func genPasswordString() string { + b := make([]byte, passwordLen) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} + +// PasswordExists checks that password exists in the memory. +func (u *Usecase) PasswordExists(password string) (bool, error) { + exists, err := u.memory.PasswordExists(password) + if err != nil { + return false, fmt.Errorf("failed to check password in the memory: %w", err) + } + return exists, nil +} diff --git a/internal/usecase/usecase.go b/internal/usecase/usecase.go new file mode 100644 index 0000000..5bb755c --- /dev/null +++ b/internal/usecase/usecase.go @@ -0,0 +1,22 @@ +package usecase + +import ( + "log/slog" + + "gommunicator/internal/communicator" + "gommunicator/internal/memory" +) + +type Usecase struct { + log *slog.Logger + memory *memory.Memory + communicator *communicator.Communicator +} + +func New(log *slog.Logger, memory *memory.Memory, communicator *communicator.Communicator) *Usecase { + return &Usecase{ + log: log, + memory: memory, + communicator: communicator, + } +} diff --git a/keys/gommunicator b/keys/gommunicator new file mode 100644 index 0000000..aff289b --- /dev/null +++ b/keys/gommunicator @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACC2W7jqv+IrEDb6dg2vR81uoUHnpCUueBZB0ajetr13hAAAAJjLUSTky1Ek +5AAAAAtzc2gtZWQyNTUxOQAAACC2W7jqv+IrEDb6dg2vR81uoUHnpCUueBZB0ajetr13hA +AAAEA1C1fC0k8MZ5S5vjcg43y2//ikwDvRbOp6GsLLAn/ZzrZbuOq/4isQNvp2Da9HzW6h +QeekJS54FkHRqN62vXeEAAAADnlhc2hAeWFzaC1hc3VzAQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/keys/gommunicator.pub b/keys/gommunicator.pub new file mode 100644 index 0000000..c087322 --- /dev/null +++ b/keys/gommunicator.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILZbuOq/4isQNvp2Da9HzW6hQeekJS54FkHRqN62vXeE yash@yash-asus