github.com/GGP1/kure@v0.8.4/commands/import/import.go (about)

     1  package importt
     2  
     3  import (
     4  	"encoding/csv"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/GGP1/kure/auth"
    11  	cmdutil "github.com/GGP1/kure/commands"
    12  	"github.com/GGP1/kure/db/entry"
    13  	"github.com/GGP1/kure/db/totp"
    14  	"github.com/GGP1/kure/pb"
    15  
    16  	"github.com/pkg/errors"
    17  	"github.com/spf13/cobra"
    18  	bolt "go.etcd.io/bbolt"
    19  )
    20  
    21  const example = `
    22  * Import
    23  kure import keepass -p path/to/file
    24  
    25  * Import and delete the file:
    26  kure import 1password -e -p path/to/file`
    27  
    28  type importOptions struct {
    29  	path  string
    30  	erase bool
    31  }
    32  
    33  // NewCmd returns a new command.
    34  func NewCmd(db *bolt.DB) *cobra.Command {
    35  	opts := importOptions{}
    36  	cmd := &cobra.Command{
    37  		Use:   "import <manager-name>",
    38  		Short: "Import entries",
    39  		Long: `Import entries from other password managers. Format: CSV.
    40  
    41  If an entry already exists it will be overwritten.
    42  
    43  Delete the CSV used with the erase flag, the file will be deleted only if no errors were encountered.
    44  
    45  Supported:
    46  	• 1Password
    47  	• Bitwarden
    48     	• Keepass/X/XC
    49  	• Lastpass`,
    50  		Example: example,
    51  		Args:    managersSupported(),
    52  		PreRunE: auth.Login(db),
    53  		RunE:    runImport(db, &opts),
    54  		PostRun: func(cmd *cobra.Command, args []string) {
    55  			// Reset variables (session)
    56  			opts = importOptions{}
    57  		},
    58  	}
    59  
    60  	f := cmd.Flags()
    61  	f.StringVarP(&opts.path, "path", "p", "", "source file path")
    62  	f.BoolVarP(&opts.erase, "erase", "e", false, "erase the file on exit (only if there are no errors)")
    63  
    64  	return cmd
    65  }
    66  
    67  func runImport(db *bolt.DB, opts *importOptions) cmdutil.RunEFunc {
    68  	return func(cmd *cobra.Command, args []string) error {
    69  		manager := strings.Join(args, " ")
    70  		manager = strings.ToLower(manager)
    71  
    72  		if opts.path == "" {
    73  			return cmdutil.ErrInvalidPath
    74  		}
    75  		ext := filepath.Ext(opts.path)
    76  		if ext == "" || ext == "." {
    77  			opts.path += ".csv"
    78  		}
    79  
    80  		records, err := readCSV(opts.path)
    81  		if err != nil {
    82  			return err
    83  		}
    84  
    85  		if err := createEntries(db, manager, records); err != nil {
    86  			return err
    87  		}
    88  
    89  		if opts.erase {
    90  			if err := cmdutil.Erase(opts.path); err != nil {
    91  				return err
    92  			}
    93  			fmt.Println("Erased file at", opts.path)
    94  		}
    95  
    96  		fmt.Println("Successfully imported the entries from", manager)
    97  		return nil
    98  	}
    99  }
   100  
   101  func createEntries(db *bolt.DB, manager string, records [][]string) error {
   102  	// [1:] used to skip headers
   103  	records = records[:][1:]
   104  	entries := make([]*pb.Entry, len(records))
   105  
   106  	switch manager {
   107  	case "keepass", "keepassx":
   108  		for i, record := range records {
   109  			entries[i] = &pb.Entry{
   110  				Name:     cmdutil.NormalizeName(record[0]),
   111  				Username: record[1],
   112  				Password: record[2],
   113  				URL:      record[3],
   114  				Notes:    record[4],
   115  				Expires:  "Never",
   116  			}
   117  		}
   118  
   119  	case "keepassxc":
   120  		for i, record := range records {
   121  			entries[i] = &pb.Entry{
   122  				// Join folder and name
   123  				Name:     cmdutil.NormalizeName(record[0] + "/" + record[1]),
   124  				Username: record[2],
   125  				Password: record[3],
   126  				URL:      record[4],
   127  				Notes:    record[5],
   128  				Expires:  "Never",
   129  			}
   130  		}
   131  
   132  	case "1password":
   133  		for i, record := range records {
   134  			entries[i] = &pb.Entry{
   135  				Name:     cmdutil.NormalizeName(record[0]),
   136  				Username: record[2],
   137  				Password: record[3],
   138  				URL:      record[1],
   139  				Notes:    fmt.Sprintf("%s.\nMember number: %s.\nRecovery Codes: %s", record[4], record[5], record[6]),
   140  				Expires:  "Never",
   141  			}
   142  		}
   143  
   144  	case "lastpass":
   145  		for i, record := range records {
   146  			entries[i] = &pb.Entry{
   147  				// Join folder and name
   148  				Name:     cmdutil.NormalizeName(record[5] + "/" + record[4]),
   149  				Username: record[1],
   150  				Password: record[2],
   151  				URL:      record[0],
   152  				Notes:    record[3],
   153  				Expires:  "Never",
   154  			}
   155  		}
   156  
   157  	case "bitwarden":
   158  		for i, record := range records {
   159  			// Join folder and name
   160  			name := cmdutil.NormalizeName(record[0] + "/" + record[3])
   161  			entries[i] = &pb.Entry{
   162  				Name:     name,
   163  				Username: record[7],
   164  				Password: record[8],
   165  				URL:      record[6],
   166  				Notes:    record[4],
   167  				Expires:  "Never",
   168  			}
   169  
   170  			// Create TOTP if the entry has one
   171  			if err := createTOTP(db, name, record[9]); err != nil {
   172  				return err
   173  			}
   174  		}
   175  	}
   176  
   177  	return entry.Create(db, entries...)
   178  }
   179  
   180  func createTOTP(db *bolt.DB, name, rawToken string) error {
   181  	if rawToken == "" {
   182  		return nil
   183  	}
   184  
   185  	t := &pb.TOTP{
   186  		Name: name,
   187  		Raw:  rawToken,
   188  		// Bitwarden uses 6 digits by default
   189  		Digits: 6,
   190  	}
   191  
   192  	return totp.Create(db, t)
   193  }
   194  
   195  func readCSV(path string) ([][]string, error) {
   196  	f, err := os.Open(path)
   197  	if err != nil {
   198  		return nil, errors.Wrap(err, "opening file")
   199  	}
   200  	defer f.Close()
   201  
   202  	fInfo, err := f.Stat()
   203  	if err != nil {
   204  		return nil, errors.Wrap(err, "obtaining file information")
   205  	}
   206  
   207  	if fInfo.Size() == 0 {
   208  		return nil, errors.New("the CSV file is empty")
   209  	}
   210  
   211  	r := csv.NewReader(f)
   212  	records, err := r.ReadAll()
   213  	if err != nil {
   214  		return nil, errors.Wrap(err, "reading csv data")
   215  	}
   216  
   217  	return records, nil
   218  }
   219  
   220  func managersSupported() cobra.PositionalArgs {
   221  	return func(cmd *cobra.Command, args []string) error {
   222  		manager := strings.Join(args, " ")
   223  
   224  		switch strings.ToLower(manager) {
   225  		case "1password", "bitwarden", "keepass", "keepassx", "keepassxc", "lastpass":
   226  
   227  		default:
   228  			return errors.Errorf(`%q is not supported
   229  
   230  Managers supported: 1Password, Bitwarden, Keepass/X/XC, Lastpass`, manager)
   231  		}
   232  		return nil
   233  	}
   234  }