Ctrl K
ComunidadGuías…

server/ssh/web/utils.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package web

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"os"

	"golang.org/x/crypto/ssh"
)

// Credentials is what the browser supplies to log in to a device. Exactly one of the password
// and the key is used, depending on the method the device accepts.
type Credentials struct {
	// Device is the device what the session is open.
	Device string `json:"device"`
	// Username is the username in the device's OS.
	Username string `json:"username"`
	// Password is the password in the device's OS.
	Password string `json:"password"`
	// Fingerprint is the identifier of the public key used in the device's OS.
	Fingerprint string `json:"fingerprint"`
	// PublicKey is the browser's own enrolled SSH public key (an "ssh-ed25519 …"
	// line) in a new namespace. When set, the web terminal authenticates by
	// signing the SSH challenge with the matching non-extractable browser key,
	// and the gateway resolves it to the identity — no device credential and no
	// legacy public-key ACL. Empty in legacy and keyless-fallback sessions.
	PublicKey string `json:"public_key"`
	// UserID is the ShellHub account driving the web terminal, taken from the
	// identity authentication resolved for the request (never from the request
	// body). It is the identity used in a new namespace, where
	// no device credential is presented; empty in a legacy namespace.
	UserID string `json:"-"`
}

func (c *Credentials) encryptPassword(key *rsa.PrivateKey) error {
	if c.Password == "" {
		return nil
	}

	signed, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &key.PublicKey, []byte(c.Password), nil)
	if err != nil {
		return errors.Join(ErrCredentialsEncryptPassword, err)
	}

	c.Password = hex.EncodeToString(signed)

	return nil
}

func (c *Credentials) decryptPassword(key *rsa.PrivateKey) error {
	if c.Password == "" {
		return nil
	}

	decoded, err := hex.DecodeString(c.Password)
	if err != nil {
		return errors.Join(errors.New("failed to decode the session's password"), err)
	}

	decrypted, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, key, decoded, nil)
	if err != nil {
		return errors.Join(errors.New("failed to decrypt the session's password"), err)
	}

	c.Password = string(decrypted)

	return nil
}

func (c *Credentials) isPublicKey() bool {
	return c.Fingerprint != ""
}

func (c *Credentials) isPassword() bool {
	return !c.isPublicKey()
}

// Dimensions represents a web SSH terminal dimensions.
//
// The sizes are 16 bits wide because that is what a pty can allocate; the width also makes
// the conversion to the int the pty request takes exact on any platform.
type Dimensions struct {
	Cols uint16 `json:"cols"`
	Rows uint16 `json:"rows"`
}

// Info is the client-supplied context recorded against the session — its originating IP,
// which the browser cannot be trusted for and which the handler overrides.
type Info struct {
	IP string `json:"ip"`
}

func readHostKey(path string) (ssh.PublicKey, error) {
	pem, err := os.ReadFile(path) //nolint:gosec // path comes from the server's own configuration, not user input.
	if err != nil {
		return nil, errors.Join(ErrBridgeReadHostKey, err)
	}

	signer, err := ssh.ParsePrivateKey(pem)
	if err != nil {
		return nil, errors.Join(ErrBridgeReadHostKey, err)
	}

	return signer.PublicKey(), nil
}