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 }