diff --git a/agent/actions/send.go b/agent/actions/send.go new file mode 100644 index 0000000..e914e42 --- /dev/null +++ b/agent/actions/send.go @@ -0,0 +1,30 @@ +package actions + +import ( + "context" + "fmt" + + "github.com/quexten/goldwarden/agent/bitwarden" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/sockets" + "github.com/quexten/goldwarden/agent/vault" + "github.com/quexten/goldwarden/ipc/messages" +) + +func handleCreateSend(msg messages.IPCMessage, cfg *config.Config, vault *vault.Vault, callingContext *sockets.CallingContext) (response messages.IPCMessage, err error) { + token, err := cfg.GetToken() + if err != nil { + return messages.IPCMessage{}, fmt.Errorf("error getting token: %w", err) + } + parsedMsg := messages.ParsePayload(msg).(messages.CreateSendRequest) + + ctx := context.WithValue(context.TODO(), bitwarden.AuthToken{}, token.AccessToken) + _, err = bitwarden.CreateSend(ctx, cfg, vault, parsedMsg.Name, parsedMsg.Text) + + response, err = messages.IPCMessageFromPayload(messages.CreateSendResponse{}) + return +} + +func init() { + AgentActionsRegistry.Register(messages.MessageTypeForEmptyPayload(messages.CreateSendRequest{}), ensureIsNotLocked(ensureIsLoggedIn(handleCreateSend))) +} diff --git a/agent/bitwarden/crypto/encstring.go b/agent/bitwarden/crypto/encstring.go index 081bed6..b61c503 100644 --- a/agent/bitwarden/crypto/encstring.go +++ b/agent/bitwarden/crypto/encstring.go @@ -218,6 +218,20 @@ func EncryptWith(data []byte, encType EncStringType, key SymmetricEncryptionKey) return s, nil } +func EncryptWithToString(data []byte, encType EncStringType, key SymmetricEncryptionKey) (string, error) { + s, err := EncryptWith(data, encType, key) + if err != nil { + return "", err + } + + marshalled, err := s.MarshalText() + if err != nil { + return "", err + } + + return string(marshalled), nil +} + func GenerateAsymmetric(useMemguard bool) (AsymmetricEncryptionKey, error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { diff --git a/agent/bitwarden/send.go b/agent/bitwarden/send.go new file mode 100644 index 0000000..d224241 --- /dev/null +++ b/agent/bitwarden/send.go @@ -0,0 +1,107 @@ +package bitwarden + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "io" + "time" + + "github.com/quexten/goldwarden/agent/bitwarden/crypto" + "github.com/quexten/goldwarden/agent/config" + "github.com/quexten/goldwarden/agent/vault" + "golang.org/x/crypto/hkdf" +) + +type SendFileMetadata struct { + FileName string `json:"fileName"` + Id string `json:"id"` + Size int `json:"size"` + SizeName string `json:"sizeName"` +} + +type SendTextMetadata struct { + Hidden bool `json:"hidden"` + Response *string `json:"response"` + Text string `json:"text"` +} + +type SendMetadata struct { + CreatorIdentifier string + ExpirationDate string + File SendFileMetadata + Id string + Name string + Object string + Text SendTextMetadata + Type int +} + +type SendCreateRequest struct { + DeletionDate string `json:"deletionDate"` + Disabled bool `json:"disabled"` + ExpirationDate *string `json:"expirationDate"` + HideEmail bool `json:"hideEmail"` + Key string `json:"key"` + MaxAccessCount *int `json:"maxAccessCount"` + Name string `json:"name"` + Notes *string `json:"notes"` + Text SendTextMetadata `json:"text"` + Type int `json:"type"` +} + +func CreateSend(ctx context.Context, cfg *config.Config, vault *vault.Vault, name string, text string) (SendMetadata, error) { + timestampIn14Days := time.Now().AddDate(0, 0, 14) + timestampIn14DaysStr := timestampIn14Days.Format("2006-01-02T15:04:05Z") + + // generate 32 byte key + sendSourceKey := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, sendSourceKey) + if err != nil { + return SendMetadata{}, err + } + + encryptedSendSourceKey, err := crypto.EncryptWithToString(sendSourceKey, crypto.AesCbc256_HmacSha256_B64, vault.Keyring.GetAccountKey()) + if err != nil { + return SendMetadata{}, err + } + + sendUseKeyPairBytes := make([]byte, 64) + hkdf.New(sha256.New, sendSourceKey, []byte("bitwarden-send"), []byte("send")).Read(sendUseKeyPairBytes) + + sendUseKeyPair, err := crypto.MemorySymmetricEncryptionKeyFromBytes(sendUseKeyPairBytes) + if err != nil { + return SendMetadata{}, err + } + + encryptedName, err := crypto.EncryptWithToString([]byte(name), crypto.AesCbc256_HmacSha256_B64, sendUseKeyPair) + if err != nil { + return SendMetadata{}, err + } + + encryptedText, err := crypto.EncryptWithToString([]byte(text), crypto.AesCbc256_HmacSha256_B64, sendUseKeyPair) + if err != nil { + return SendMetadata{}, err + } + + sendRequest := SendCreateRequest{ + DeletionDate: timestampIn14DaysStr, + Disabled: false, + HideEmail: false, + Key: encryptedSendSourceKey, + Name: encryptedName, + Text: SendTextMetadata{ + Hidden: false, + Text: encryptedText, + }, + Type: 0, + } + + var result interface{} + err = authenticatedHTTPPost(ctx, cfg.ConfigFile.ApiUrl+"/sends", &result, sendRequest) + if err != nil { + return SendMetadata{}, err + } + + return SendMetadata{}, nil +} diff --git a/cmd/send.go b/cmd/send.go new file mode 100644 index 0000000..aa098a0 --- /dev/null +++ b/cmd/send.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "fmt" + + "github.com/quexten/goldwarden/ipc/messages" + "github.com/spf13/cobra" +) + +var sendCmd = &cobra.Command{ + Use: "send", + Short: "Commands for managing sends", + Long: `Commands for managing sends.`, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +var sendCreateCmd = &cobra.Command{ + Use: "create", + Short: "Uploads a Bitwarden send.", + Long: `Uploads a Bitwarden send.`, + Run: func(cmd *cobra.Command, args []string) { + loginIfRequired() + name, _ := cmd.Flags().GetString("name") + text, _ := cmd.Flags().GetString("text") + + result, err := commandClient.SendToAgent(messages.CreateSendRequest{ + Name: name, + Text: text, + }) + if err != nil { + handleSendToAgentError(err) + return + } + + switch result.(type) { + case messages.CreateSendResponse: + fmt.Println("Send created: " + result.(messages.CreateSendResponse).URL) + break + case messages.ActionResponse: + println("Error: " + result.(messages.ActionResponse).Message) + return + } + }, +} + +func init() { + rootCmd.AddCommand(sendCmd) + sendCmd.AddCommand(sendCreateCmd) + sendCreateCmd.Flags().StringP("name", "n", "", "Name of the send") + sendCreateCmd.Flags().StringP("text", "t", "", "Text of the send") +} diff --git a/ipc/messages/send.go b/ipc/messages/send.go new file mode 100644 index 0000000..d5aff5f --- /dev/null +++ b/ipc/messages/send.go @@ -0,0 +1,72 @@ +package messages + +import "encoding/json" + +type GetSendRequest struct { + Name string + Text string +} + +type GetSendResponse struct { + Found bool + Text string +} + +type CreateSendRequest struct { + Name string + Text string +} + +type CreateSendResponse struct { + URL string +} + +type ListSendsRequest struct { +} + +func init() { + registerPayloadParser(func(payload []byte) (interface{}, error) { + var req GetSendRequest + err := json.Unmarshal(payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req, nil + }, GetSendRequest{}) + + registerPayloadParser(func(payload []byte) (interface{}, error) { + var req GetSendResponse + err := json.Unmarshal(payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req, nil + }, GetSendResponse{}) + + registerPayloadParser(func(payload []byte) (interface{}, error) { + var req CreateSendRequest + err := json.Unmarshal(payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req, nil + }, CreateSendRequest{}) + + registerPayloadParser(func(payload []byte) (interface{}, error) { + var req CreateSendResponse + err := json.Unmarshal(payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req, nil + }, CreateSendResponse{}) + + registerPayloadParser(func(payload []byte) (interface{}, error) { + var req ListSendsRequest + err := json.Unmarshal(payload, &req) + if err != nil { + panic("Unmarshal: " + err.Error()) + } + return req, nil + }, ListSendsRequest{}) +}