1package config 2 3import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "sort" 8 9 "github.com/gopasspw/gopass/pkg/debug" 10 "github.com/gopasspw/gopass/pkg/fsutil" 11 12 "gopkg.in/yaml.v3" 13) 14 15// LoadWithFallbackRelaxed will try to load the config from one of the default 16// locations but also accept a more recent config. 17func LoadWithFallbackRelaxed() *Config { 18 return loadWithFallback(true) 19} 20 21// LoadWithFallback will try to load the config from one of the default locations 22func LoadWithFallback() *Config { 23 return loadWithFallback(false) 24} 25 26func loadWithFallback(relaxed bool) *Config { 27 for _, l := range configLocations() { 28 if cfg := loadConfig(l, relaxed); cfg != nil { 29 return cfg 30 } 31 } 32 return loadDefault() 33} 34 35// Load will load the config from the default location or return a default config 36func Load() *Config { 37 if cfg := loadConfig(configLocation(), false); cfg != nil { 38 return cfg 39 } 40 return loadDefault() 41} 42 43func loadConfig(l string, relaxed bool) *Config { 44 debug.Log("Trying to load config from %s", l) 45 cfg, err := load(l, relaxed) 46 if err == ErrConfigNotFound { 47 return nil 48 } 49 if err != nil { 50 return nil 51 } 52 debug.Log("Loaded config from %s: %+v", l, cfg) 53 return cfg 54} 55 56func loadDefault() *Config { 57 cfg := New() 58 cfg.Path = PwStoreDir("") 59 debug.Log("Loaded default config: %+v", cfg) 60 return cfg 61} 62 63func load(cf string, relaxed bool) (*Config, error) { 64 // deliberately using os.Stat here, a symlinked 65 // config is OK 66 if _, err := os.Stat(cf); err != nil { 67 return nil, ErrConfigNotFound 68 } 69 buf, err := os.ReadFile(cf) 70 if err != nil { 71 fmt.Fprintf(os.Stderr, "Error reading config from %s: %s\n", cf, err) 72 return nil, ErrConfigNotFound 73 } 74 75 cfg, err := decode(buf, relaxed) 76 if err != nil { 77 fmt.Fprintf(os.Stderr, "Error reading config from %s: %s\n", cf, err) 78 return nil, ErrConfigNotParsed 79 } 80 if cfg.Mounts == nil { 81 cfg.Mounts = make(map[string]string) 82 } 83 cfg.ConfigPath = cf 84 return cfg, nil 85} 86 87func checkOverflow(m map[string]interface{}) error { 88 if len(m) < 1 { 89 return nil 90 } 91 92 var keys []string 93 for k := range m { 94 keys = append(keys, k) 95 } 96 sort.Strings(keys) 97 return fmt.Errorf("unknown fields: %+v", keys) 98} 99 100type configer interface { 101 Config() *Config 102 CheckOverflow() error 103} 104 105func decode(buf []byte, relaxed bool) (*Config, error) { 106 mostRecent := &Config{ 107 AutoImport: true, 108 ClipTimeout: 45, 109 ExportKeys: true, 110 Notifications: true, 111 Parsing: true, 112 Path: PwStoreDir(""), 113 } 114 cfgs := []configer{ 115 // most recent config must come first 116 mostRecent, 117 &Pre1127{}, 118 &Pre1102{}, 119 &Pre193{ 120 Root: &Pre193StoreConfig{}, 121 }, 122 &Pre182{ 123 Root: &Pre182StoreConfig{}, 124 }, 125 &Pre140{}, 126 &Pre130{}, 127 } 128 if relaxed { 129 // most recent config must come last as well, will be tried w/o 130 // overflow checks 131 cfgs = append(cfgs, mostRecent) 132 } 133 var warn string 134 for i, cfg := range cfgs { 135 debug.Log("Trying to unmarshal config into %T", cfg) 136 if err := yaml.Unmarshal(buf, cfg); err != nil { 137 debug.Log("Loading config %T failed: %s", cfg, err) 138 continue 139 } 140 if err := cfg.CheckOverflow(); err != nil { 141 debug.Log("Extra elements in config: %s", err) 142 if i == 0 { 143 warn = fmt.Sprintf("Failed to load config %T. Do you need to remove deprecated fields? %s\n", cfg, err) 144 } 145 // usually we are strict about extra fields, i.e. any field left 146 // unparsed means this config failed and we try the next one. 147 if i < len(cfgs)-1 { 148 continue 149 } 150 // in relaxed mode we append an extra copy of the most recent 151 // config to the end of the slice and might just ignore these 152 // extra fields. 153 if !relaxed { 154 continue 155 } 156 debug.Log("Ignoring extra config fields for fallback config (only)") 157 } 158 debug.Log("Loaded config: %T: %+v", cfg, cfg) 159 conf := cfg.Config() 160 if i > 0 { 161 debug.Log("Loaded legacy config. Should rewrite config.") 162 } 163 return conf, nil 164 } 165 // We try to provide a seamless config upgrade path for users of our 166 // released versions. But some users build gopass from the master branch 167 // and these might run into issues when we remove config options. 168 // Since our config parser is pedantic (it has to) we fail parsing on 169 // unknown config options. If we remove one and the user rebuilds it's 170 // gopass binary without changing the config, it will fail to parse the 171 // current config and the legacy configs will likely fail as well. 172 // But if we always display the warning users with configs from previous 173 // releases will always see the warning. So instead we only display the 174 // warning if parsing of the most up to date config struct fails AND 175 // not other succeeds. 176 if warn != "" { 177 fmt.Fprint(os.Stderr, warn) 178 } 179 return nil, ErrConfigNotParsed 180} 181 182// Save saves the config 183func (c *Config) Save() error { 184 buf, err := yaml.Marshal(c) 185 if err != nil { 186 return fmt.Errorf("failed to marshal YAML: %w", err) 187 } 188 189 cfgLoc := configLocation() 190 cfgDir := filepath.Dir(cfgLoc) 191 if !fsutil.IsDir(cfgDir) { 192 if err := os.MkdirAll(cfgDir, 0700); err != nil { 193 return fmt.Errorf("failed to create dir %q: %w", cfgDir, err) 194 } 195 } 196 if err := os.WriteFile(cfgLoc, buf, 0600); err != nil { 197 return fmt.Errorf("failed to write config file to %q: %w", cfgLoc, err) 198 } 199 debug.Log("Saved config to %s: %+v\n", cfgLoc, c) 200 return nil 201} 202