1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package logic

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"rss-kakapo/dal"
	"rss-kakapo/shared"
	"sort"
)

// MigrateEncryptedPrivKeys rewrites every account's private key that is still
// stored encrypted as a plain PEM block, decrypting it with the passphrase
// from the secrets file.
//
// Keys used to be encrypted with birb_privkey_passphrase, which bought almost
// nothing and cost a secret that could never be lost (see
// keyStore.MakeKeyPair for why it went). Those keys are an instance's
// accounts: they cannot be regenerated, since other servers have cached their
// public halves, so they are converted rather than replaced.
//
// It runs at every startup and does nothing — without reading a key, without
// touching the database — once no key is encrypted, which is the state right
// after the first run. The passphrase is only needed while there is something
// to convert, and nothing but this reads it any more.
//
// Each key is written on its own, so a key that cannot be decrypted (a
// passphrase that was changed under the instance, say) leaves the rest
// converted and is named in the error.
func MigrateEncryptedPrivKeys(cfg *shared.Config, repo dal.IRepo, logger shared.ILogger) error {

	keys, err := repo.ListPrivKeys()
	if err != nil {
		return err
	}

	var encrypted []string
	for handle, privKey := range keys {
		block, _ := pem.Decode([]byte(privKey))
		if block != nil && x509.IsEncryptedPEMBlock(block) {
			encrypted = append(encrypted, handle)
		}
	}
	if len(encrypted) == 0 {
		return nil
	}
	sort.Strings(encrypted)

	pass := []byte(cfg.Secrets.BirbPrivKeyPass)
	logger.Printf("Converting %d encrypted private key(s) to plain storage", len(encrypted))

	var failed []string
	for _, handle := range encrypted {
		block, _ := pem.Decode([]byte(keys[handle]))
		keyBytes, err := x509.DecryptPEMBlock(block, pass)
		if err != nil {
			logger.Errorf("Failed to decrypt private key of '%s': %v", handle, err)
			failed = append(failed, handle)
			continue
		}
		// Parsed before it is stored: a wrong passphrase can decrypt to
		// garbage without erroring (the format authenticates nothing), and
		// storing that would lose the key for good.
		if _, err := x509.ParsePKCS1PrivateKey(keyBytes); err != nil {
			logger.Errorf("Decrypted private key of '%s' is not a key: %v", handle, err)
			failed = append(failed, handle)
			continue
		}
		plain := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes})
		if err := repo.SetPrivKey(handle, string(plain)); err != nil {
			return fmt.Errorf("storing private key of '%s': %w", handle, err)
		}
	}
	if len(failed) != 0 {
		return fmt.Errorf("could not decrypt the private key of %v; is birb_privkey_passphrase the one they were encrypted with?", failed)
	}
	logger.Printf("Converted %d private key(s); birb_privkey_passphrase is no longer needed", len(encrypted))
	return nil
}