package cmd import ( "crypto" "crypto/x509" "encoding/json" "encoding/pem" "errors" "net/url" "os" "path/filepath" "strings" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/registration" "github.com/urfave/cli/v2" ) const ( baseAccountsRootFolderName = "accounts" baseKeysFolderName = "keys" accountFileName = "account.json" ) // AccountsStorage A storage for account data. // // rootPath: // // ./.lego/accounts/ // │ └── root accounts directory // └── "path" option // // rootUserPath: // // ./.lego/accounts/localhost_14000/hubert@hubert.com/ // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) // │ └── root accounts directory // └── "path" option // // keysPath: // // ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/ // │ │ │ │ └── root keys directory // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) // │ └── root accounts directory // └── "path" option // // accountFilePath: // // ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json // │ │ │ │ └── account file // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) // │ └── root accounts directory // └── "path" option type AccountsStorage struct { userID string rootPath string rootUserPath string keysPath string accountFilePath string ctx *cli.Context } // NewAccountsStorage Creates a new AccountsStorage. func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { // TODO: move to account struct? Currently MUST pass email. email := getEmail(ctx) serverURL, err := url.Parse(ctx.String(flgServer)) if err != nil { log.Fatal(err) } rootPath := filepath.Join(ctx.String(flgPath), baseAccountsRootFolderName) serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host) accountsPath := filepath.Join(rootPath, serverPath) rootUserPath := filepath.Join(accountsPath, email) return &AccountsStorage{ userID: email, rootPath: rootPath, rootUserPath: rootUserPath, keysPath: filepath.Join(rootUserPath, baseKeysFolderName), accountFilePath: filepath.Join(rootUserPath, accountFileName), ctx: ctx, } } func (s *AccountsStorage) ExistsAccountFilePath() bool { accountFile := filepath.Join(s.rootUserPath, accountFileName) if _, err := os.Stat(accountFile); os.IsNotExist(err) { return false } else if err != nil { log.Fatal(err) } return true } func (s *AccountsStorage) GetRootPath() string { return s.rootPath } func (s *AccountsStorage) GetRootUserPath() string { return s.rootUserPath } func (s *AccountsStorage) GetUserID() string { return s.userID } func (s *AccountsStorage) Save(account *Account) error { jsonBytes, err := json.MarshalIndent(account, "", "\t") if err != nil { return err } return os.WriteFile(s.accountFilePath, jsonBytes, filePerm) } func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { fileBytes, err := os.ReadFile(s.accountFilePath) if err != nil { log.Fatalf("Could not load file for account %s: %v", s.userID, err) } var account Account err = json.Unmarshal(fileBytes, &account) if err != nil { log.Fatalf("Could not parse file for account %s: %v", s.userID, err) } account.key = privateKey if account.Registration == nil || account.Registration.Body.Status == "" { reg, err := tryRecoverRegistration(s.ctx, privateKey) if err != nil { log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.userID, err) } account.Registration = reg err = s.Save(&account) if err != nil { log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.userID, err) } } return &account } func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey { accKeyPath := filepath.Join(s.keysPath, s.userID+".key") if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType) s.createKeysFolder() privateKey, err := generatePrivateKey(accKeyPath, keyType) if err != nil { log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err) } log.Printf("Saved key to %s", accKeyPath) return privateKey } privateKey, err := loadPrivateKey(accKeyPath) if err != nil { log.Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) } return privateKey } func (s *AccountsStorage) createKeysFolder() { if err := createNonExistingFolder(s.keysPath); err != nil { log.Fatalf("Could not check/create directory for account %s: %v", s.userID, err) } } func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) { privateKey, err := certcrypto.GeneratePrivateKey(keyType) if err != nil { return nil, err } certOut, err := os.Create(file) if err != nil { return nil, err } defer certOut.Close() pemKey := certcrypto.PEMBlock(privateKey) err = pem.Encode(certOut, pemKey) if err != nil { return nil, err } return privateKey, nil } func loadPrivateKey(file string) (crypto.PrivateKey, error) { keyBytes, err := os.ReadFile(file) if err != nil { return nil, err } keyBlock, _ := pem.Decode(keyBytes) switch keyBlock.Type { case "RSA PRIVATE KEY": return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) case "EC PRIVATE KEY": return x509.ParseECPrivateKey(keyBlock.Bytes) } return nil, errors.New("unknown private key type") } func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) { // couldn't load account but got a key. Try to look the account up. config := lego.NewConfig(&Account{key: privateKey}) config.CADirURL = ctx.String(flgServer) config.UserAgent = getUserAgent(ctx) client, err := lego.NewClient(config) if err != nil { return nil, err } reg, err := client.Registration.ResolveAccountByKey() if err != nil { return nil, err } return reg, nil }