waveterm/wavesrv/pkg/waveenc/waveenc.go
Sylvie Crowe 2913babea7
Sudo Caching (#573)
* feat: share sudo between pty sessions

This is a first pass at a feature to cache the sudo password and share
it between different pty sessions. This makes it possible to not require
manual password entry every time sudo is used.

* feat: allow error handling and canceling sudo cmds

This adds the missing functionality that prevented failed sudo commands
from automatically closing.

* feat: restrict sudo caching to dev mode for now

* modify fullCmdStr not pk.Command

* refactor: condense ecdh encryptor creation

This refactors the common pieces needed to create an encryptor from an
ecdh key pair into a common function.

* refactor: rename promptenc to waveenc

* feat: add command to clear sudo password

We currently do not provide use of the sudo -k and sudo -K commands to
clear the sudo password. This adds a /sudo:clear command to handle it in
the meantime.

* feat: add kwarg to force sudo

In cases where parsing for sudo doesn't work, this provides an alternate
wave kwarg to use instead. It can be used with [sudo=1] at the beginning
of a command.

* refactor: simplify sudoArg parsing

* feat: allow user to clear all sudo passwords

This introduces the "all" kwarg for the sudo:clear command in order to
clear all sudo passwords.

* fix: handle deadline with real time

Golang's time module uses monatomic time by default, but that is not
desired for the password timeout since we want the timer to continue
even if the computer is asleep. We now avoid this by directly comparing
the unix timestamps.

* fix: remove sudo restriction to dev mode

This allows it to be used in regular builds as well.

* fix: switch to password timeout without wait group

This removes an unnecessary waiting period for sudo password entry.

* fix: update deadline in sudo:clear

This allows sudo:clear to cancel the goroutine for watching the password
timer.

* fix: pluralize sudo:clear message when all=1

This changes the output message for /sudo:clear to indicate multiple
passwords cleared if the all=1 kwarg is used.

* fix: use GetRemoteMap for getting remotes in clear

The sudo:clear command was directly looping over the GlobalStore.Map
which is not thread safe. Switched to GetRemoteMap which uses a lock
internally.

* fix: allow sudo metacmd to set sudo false

This fixes the logic for resolving if a command is a sudo command. This
change makes it possible for the sudo metacmd kwarg to force sudo to be
false.
2024-04-16 16:58:17 -07:00

228 lines
5.8 KiB
Go

// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package waveenc
import (
"crypto/cipher"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"reflect"
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
ccp "golang.org/x/crypto/chacha20poly1305"
)
const EncTagName = "enc"
const EncFieldIndicator = "*"
type Encryptor struct {
Key []byte
AEAD cipher.AEAD
}
type HasOData interface {
GetOData() string
}
func readRandBytes(n int) ([]byte, error) {
rtn := make([]byte, n)
_, err := io.ReadFull(rand.Reader, rtn)
return rtn, err
}
func MakeRandomEncryptor() (*Encryptor, error) {
key, err := readRandBytes(ccp.KeySize)
if err != nil {
return nil, err
}
rtn := &Encryptor{Key: key}
rtn.AEAD, err = ccp.NewX(rtn.Key)
if err != nil {
return nil, err
}
return rtn, nil
}
func MakeEncryptor(key []byte) (*Encryptor, error) {
var err error
rtn := &Encryptor{Key: key}
rtn.AEAD, err = ccp.NewX(rtn.Key)
if err != nil {
return nil, err
}
return rtn, nil
}
func MakeEncryptorB64(key64 string) (*Encryptor, error) {
keyBytes, err := base64.RawURLEncoding.DecodeString(key64)
if err != nil {
return nil, err
}
return MakeEncryptor(keyBytes)
}
func (enc *Encryptor) EncryptData(plainText []byte, odata string) ([]byte, error) {
bufSize, err := utilfn.AddIntSlice(enc.AEAD.NonceSize(), enc.AEAD.Overhead(), len(plainText))
if err != nil {
return nil, err
}
outputBuf := make([]byte, bufSize)
nonce := outputBuf[0:enc.AEAD.NonceSize()]
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return nil, err
}
// we're going to append the cipherText to nonce. so the encrypted data is [nonce][ciphertext]
// note that outputbuf should be the correct size to hold the rtn value
rtn := enc.AEAD.Seal(nonce, nonce, plainText, []byte(odata))
return rtn, nil
}
func (enc *Encryptor) DecryptData(encData []byte, odata string) ([]byte, error) {
minLen := enc.AEAD.NonceSize() + enc.AEAD.Overhead()
if len(encData) < minLen {
return nil, fmt.Errorf("invalid encdata, len:%d is less than minimum len:%d", len(encData), minLen)
}
nonce := encData[0:enc.AEAD.NonceSize()]
cipherText := encData[enc.AEAD.NonceSize():]
return enc.AEAD.Open(nil, nonce, cipherText, []byte(odata))
}
type EncryptMeta struct {
EncField *reflect.StructField
PlainFields map[string]reflect.StructField
}
func isByteArrayType(t reflect.Type) bool {
return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8
}
func metaFromType(v interface{}) (*EncryptMeta, error) {
if v == nil {
return nil, fmt.Errorf("Encryptor cannot encrypt nil")
}
rt := reflect.TypeOf(v)
if rt.Kind() != reflect.Pointer {
return nil, fmt.Errorf("Encryptor invalid type %T, not a pointer type", v)
}
rtElem := rt.Elem()
if rtElem.Kind() != reflect.Struct {
return nil, fmt.Errorf("Encryptor invalid type %T, not a pointer to struct type", v)
}
meta := &EncryptMeta{}
meta.PlainFields = make(map[string]reflect.StructField)
numFields := rtElem.NumField()
for i := 0; i < numFields; i++ {
field := rtElem.Field(i)
encTag := field.Tag.Get(EncTagName)
if encTag == "" {
continue
}
if encTag == EncFieldIndicator {
if meta.EncField != nil {
return nil, fmt.Errorf("Encryptor, type %T has two enc fields set (*)", v)
}
if !isByteArrayType(field.Type) {
return nil, fmt.Errorf("Encryptor, type %T enc field %q is not []byte", v, field.Name)
}
meta.EncField = &field
continue
}
if _, found := meta.PlainFields[encTag]; found {
return nil, fmt.Errorf("Encryptor, type %T has two enc fields with tag %q", v, encTag)
}
meta.PlainFields[encTag] = field
}
if meta.EncField == nil {
return nil, fmt.Errorf("Encryptor, type %T has no enc (*) field", v)
}
return meta, nil
}
func (enc *Encryptor) EncryptODS(v HasOData) error {
odata := v.GetOData()
return enc.EncryptStructFields(v, odata)
}
func (enc *Encryptor) DecryptODS(v HasOData) error {
odata := v.GetOData()
return enc.DecryptStructFields(v, odata)
}
func (enc *Encryptor) EncryptStructFields(v interface{}, odata string) error {
encMeta, err := metaFromType(v)
if err != nil {
return err
}
rvPtr := reflect.ValueOf(v)
rv := rvPtr.Elem()
m := make(map[string]interface{})
for jsonKey, field := range encMeta.PlainFields {
fieldVal := rv.FieldByIndex(field.Index)
m[jsonKey] = fieldVal.Interface()
}
barr, err := json.Marshal(m)
if err != nil {
return err
}
cipherText, err := enc.EncryptData(barr, odata)
if err != nil {
return err
}
encFieldValue := rv.FieldByIndex(encMeta.EncField.Index)
encFieldValue.SetBytes(cipherText)
return nil
}
func (enc *Encryptor) DecryptStructFields(v interface{}, odata string) error {
encMeta, err := metaFromType(v)
if err != nil {
return err
}
rvPtr := reflect.ValueOf(v)
rv := rvPtr.Elem()
cipherText := rv.FieldByIndex(encMeta.EncField.Index).Bytes()
decrypted, err := enc.DecryptData(cipherText, odata)
if err != nil {
return err
}
m := make(map[string]interface{})
err = json.Unmarshal(decrypted, &m)
if err != nil {
return err
}
for jsonKey, field := range encMeta.PlainFields {
val := m[jsonKey]
rv.FieldByIndex(field.Index).Set(reflect.ValueOf(val))
}
return nil
}
func MakeEncryptorEcdh(localPrivKey *ecdh.PrivateKey, remotePubKey []byte) (*Encryptor, error) {
shellPubKey, err := x509.ParsePKIXPublicKey(remotePubKey)
if err != nil {
return nil, fmt.Errorf("parse pub key: %e", err)
}
ecdhShellPubKey, err := shellPubKey.(*ecdsa.PublicKey).ECDH()
if err != nil {
return nil, fmt.Errorf("convert pub key from ecdsa to ecdh: %e", err)
}
sharedKey, err := localPrivKey.ECDH(ecdhShellPubKey)
if err != nil {
return nil, fmt.Errorf("compute shared key: %e", err)
}
encryptor, err := MakeEncryptor(sharedKey)
if err != nil {
return nil, fmt.Errorf("create encryptor: %e", err)
}
return encryptor, nil
}