mirror of
https://github.com/passbolt/go-passbolt-cli.git
synced 2025-05-11 02:28:22 +00:00
229 lines
6.2 KiB
Go
229 lines
6.2 KiB
Go
package keepass
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/passbolt/go-passbolt-cli/util"
|
|
"github.com/passbolt/go-passbolt/api"
|
|
"github.com/passbolt/go-passbolt/helper"
|
|
"github.com/pterm/pterm"
|
|
"github.com/spf13/cobra"
|
|
"github.com/tobischo/gokeepasslib/v3"
|
|
w "github.com/tobischo/gokeepasslib/v3/wrappers"
|
|
)
|
|
|
|
// KeepassExportCmd Exports a Passbolt Keepass
|
|
var KeepassExportCmd = &cobra.Command{
|
|
Use: "keepass",
|
|
Short: "Exports Passbolt to a Keepass File",
|
|
Long: `Exports Passbolt to a Keepass File`,
|
|
Aliases: []string{},
|
|
RunE: KeepassExport,
|
|
}
|
|
|
|
func init() {
|
|
KeepassExportCmd.Flags().StringP("file", "f", "passbolt-export.kdbx", "File name of the Keepass File")
|
|
KeepassExportCmd.Flags().StringP("password", "p", "", "Password for the Keypass File, if empty prompts interactively")
|
|
}
|
|
|
|
func KeepassExport(cmd *cobra.Command, args []string) error {
|
|
filename, err := cmd.Flags().GetString("file")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if filename == "" {
|
|
return fmt.Errorf("the Filename cannot be empty")
|
|
}
|
|
|
|
keepassPassword, err := cmd.Flags().GetString("password")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := util.GetContext()
|
|
|
|
client, err := util.GetClient(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Logout(context.TODO())
|
|
cmd.SilenceUsage = true
|
|
|
|
if keepassPassword == "" {
|
|
pw, err := util.ReadPassword("Enter Keepass Password:")
|
|
if err != nil {
|
|
fmt.Println()
|
|
return fmt.Errorf("Reading Keepass Password: %w", err)
|
|
}
|
|
keepassPassword = pw
|
|
fmt.Println()
|
|
}
|
|
|
|
fmt.Println("Getting Resources...")
|
|
resources, err := client.GetResources(ctx, &api.GetResourcesOptions{
|
|
ContainSecret: true,
|
|
ContainResourceType: true,
|
|
ContainTags: true,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("Getting Resources: %w", err)
|
|
}
|
|
|
|
file, err := os.Create(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("Creating File: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
rootGroup := gokeepasslib.NewGroup()
|
|
rootGroup.Name = "root"
|
|
|
|
pterm.EnableStyling()
|
|
pterm.DisableColor()
|
|
progressbar, err := pterm.DefaultProgressbar.WithTitle("Decryping Resources").WithTotal(len(resources)).Start()
|
|
if err != nil {
|
|
return fmt.Errorf("Progress: %w", err)
|
|
}
|
|
|
|
for _, resource := range resources {
|
|
entry, err := getKeepassEntry(client, resource, resource.Secrets[0], resource.ResourceType)
|
|
if err != nil {
|
|
fmt.Printf("\nSkipping Export of Resource %v %v Because of: %v\n", resource.ID, resource.Name, err)
|
|
progressbar.Increment()
|
|
continue
|
|
}
|
|
|
|
rootGroup.Entries = append(rootGroup.Entries, *entry)
|
|
progressbar.Increment()
|
|
}
|
|
|
|
db := gokeepasslib.NewDatabase(
|
|
gokeepasslib.WithDatabaseKDBXVersion4(),
|
|
)
|
|
db.Content.Meta.DatabaseName = "Passbolt Export"
|
|
|
|
if keepassPassword != "" {
|
|
db.Credentials = gokeepasslib.NewPasswordCredentials(keepassPassword)
|
|
}
|
|
|
|
db.Content.Root = &gokeepasslib.RootData{
|
|
Groups: []gokeepasslib.Group{rootGroup},
|
|
}
|
|
|
|
db.LockProtectedEntries()
|
|
|
|
keepassEncoder := gokeepasslib.NewEncoder(file)
|
|
if err := keepassEncoder.Encode(db); err != nil {
|
|
return fmt.Errorf("Encodeing kdbx: %w", err)
|
|
}
|
|
fmt.Println("Done")
|
|
|
|
return nil
|
|
}
|
|
|
|
func getKeepassEntry(client *api.Client, resource api.Resource, secret api.Secret, rType api.ResourceType) (*gokeepasslib.Entry, error) {
|
|
_, _, _, _, pass, desc, err := helper.GetResourceFromData(client, resource, resource.Secrets[0], resource.ResourceType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Get Resource %v: %w", resource.ID, err)
|
|
}
|
|
|
|
entry := gokeepasslib.NewEntry()
|
|
entry.Values = append(
|
|
entry.Values,
|
|
gokeepasslib.ValueData{Key: "Title", Value: gokeepasslib.V{Content: resource.Name}},
|
|
gokeepasslib.ValueData{Key: "UserName", Value: gokeepasslib.V{Content: resource.Username}},
|
|
gokeepasslib.ValueData{Key: "URL", Value: gokeepasslib.V{Content: resource.URI}},
|
|
gokeepasslib.ValueData{Key: "Password", Value: gokeepasslib.V{Content: pass, Protected: w.NewBoolWrapper(true)}},
|
|
gokeepasslib.ValueData{Key: "Notes", Value: gokeepasslib.V{Content: desc}},
|
|
)
|
|
|
|
if resource.ResourceType.Slug == "password-description-totp" || resource.ResourceType.Slug == "totp" {
|
|
var totpData api.SecretDataTOTP
|
|
|
|
rawSecretData, err := client.DecryptMessage(resource.Secrets[0].Data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Decrypting Secret Data: %w", err)
|
|
}
|
|
|
|
if resource.ResourceType.Slug == "password-description-totp" {
|
|
var secretData api.SecretDataTypePasswordDescriptionTOTP
|
|
err = json.Unmarshal([]byte(rawSecretData), &secretData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
|
|
}
|
|
totpData = secretData.TOTP
|
|
} else {
|
|
var secretData api.SecretDataTypeTOTP
|
|
err = json.Unmarshal([]byte(rawSecretData), &secretData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
|
|
}
|
|
totpData = secretData.TOTP
|
|
}
|
|
|
|
v := url.Values{}
|
|
v.Set("secret", totpData.SecretKey)
|
|
v.Set("period", strconv.FormatUint(uint64(totpData.Period), 10))
|
|
v.Set("algorithm", totpData.Algorithm)
|
|
v.Set("digits", fmt.Sprint(totpData.Digits))
|
|
|
|
issuer := resource.URI
|
|
if resource.URI == "" {
|
|
issuer = resource.Name
|
|
|
|
}
|
|
v.Set("issuer", issuer)
|
|
|
|
accountName := resource.Username
|
|
if resource.Username == "" {
|
|
accountName = resource.Name
|
|
}
|
|
|
|
u := url.URL{
|
|
Scheme: "otpauth",
|
|
Host: "totp",
|
|
Path: "/" + issuer + ":" + accountName,
|
|
RawQuery: encodeQuery(v),
|
|
}
|
|
|
|
entry.Values = append(entry.Values, gokeepasslib.ValueData{Key: "otp", Value: gokeepasslib.V{Content: u.String(), Protected: w.NewBoolWrapper(true)}})
|
|
}
|
|
|
|
return &entry, nil
|
|
}
|
|
|
|
// EncodeQuery is a copy-paste of url.Values.Encode, except it uses %20 instead
|
|
// of + to encode spaces. This is necessary to correctly render spaces in some
|
|
// authenticator apps, like Google Authenticator.
|
|
func encodeQuery(v url.Values) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
var buf strings.Builder
|
|
keys := make([]string, 0, len(v))
|
|
for k := range v {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
vs := v[k]
|
|
keyEscaped := url.PathEscape(k) // changed from url.QueryEscape
|
|
for _, v := range vs {
|
|
if buf.Len() > 0 {
|
|
buf.WriteByte('&')
|
|
}
|
|
buf.WriteString(keyEscaped)
|
|
buf.WriteByte('=')
|
|
buf.WriteString(url.PathEscape(v)) // changed from url.QueryEscape
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|