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 }