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
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package logic

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"io"
	"path/filepath"
	"rss-kakapo/dal"
	"rss-kakapo/shared"
	"testing"
	"time"
)

const migPass = "the old passphrase"

// encryptedKey returns an RSA key in the legacy encrypted PEM form keys used
// to be stored in, with its public half.
func encryptedKey(t *testing.T, pass string) (privPem string, pub *rsa.PublicKey) {
	t.Helper()
	key, err := rsa.GenerateKey(rand.Reader, 1024) // small: only its bytes matter here
	if err != nil {
		t.Fatal(err)
	}
	block, err := x509.EncryptPEMBlock(rand.Reader, "RSA PRIVATE KEY",
		x509.MarshalPKCS1PrivateKey(key), []byte(pass), x509.PEMCipherAES256)
	if err != nil {
		t.Fatal(err)
	}
	return string(pem.EncodeToMemory(block)), &key.PublicKey
}

// migRepo returns a fresh database holding one account per entry of keys,
// handle to private key as stored.
func migRepo(t *testing.T, keys map[string]string) (*shared.Config, dal.IRepo) {
	t.Helper()
	cfg := &shared.Config{
		Host:   "kakapo.example.org",
		DbFile: filepath.Join(t.TempDir(), "test.db"),
		Birb:   &shared.UserInfo{User: "birb"},
	}
	cfg.Secrets.BirbPrivKeyPass = migPass
	repo := dal.NewRepo(cfg, shared.NewLogger(io.Discard, "Error"))
	repo.InitUpdateDb()
	for handle, privKey := range keys {
		if _, err := repo.AddAccountIfNotExist(&dal.Account{
			CreatedAt: time.Now().UTC(), Handle: handle,
			UserUrl: "https://kakapo.example.org/u/" + handle,
		}, privKey); err != nil {
			t.Fatal(err)
		}
	}
	return cfg, repo
}

func TestMigrateEncryptedPrivKeysConvertsAndKeepsTheKey(t *testing.T) {
	encrypted, pub := encryptedKey(t, migPass)
	plain, _ := encryptedKey(t, migPass) // stored encrypted too, a second account
	cfg, repo := migRepo(t, map[string]string{
		"blog.example.org":  encrypted,
		"other.example.org": plain,
	})

	if err := MigrateEncryptedPrivKeys(cfg, repo, shared.NewLogger(io.Discard, "Error")); err != nil {
		t.Fatalf("MigrateEncryptedPrivKeys: %v", err)
	}

	// The stored key is now readable without the passphrase, and is the same
	// key: an account whose key changed would lose its identity.
	stored, err := repo.GetPrivKey("blog.example.org")
	if err != nil {
		t.Fatal(err)
	}
	key, err := ParsePrivKey(stored)
	if err != nil {
		t.Fatalf("ParsePrivKey after migration: %v", err)
	}
	if !key.PublicKey.Equal(pub) {
		t.Error("migrated key is not the key that was stored")
	}
	for _, handle := range []string{"blog.example.org", "other.example.org"} {
		s, err := repo.GetPrivKey(handle)
		if err != nil {
			t.Fatal(err)
		}
		if block, _ := pem.Decode([]byte(s)); block == nil || x509.IsEncryptedPEMBlock(block) {
			t.Errorf("%s: key is still encrypted", handle)
		}
	}
}

func TestMigrateEncryptedPrivKeysLeavesPlainKeysAlone(t *testing.T) {
	cfg, repo := migRepo(t, map[string]string{"blog.example.org": ""})
	// A plain key, as MakeKeyPair writes it.
	ks := NewKeyStore(cfg, repo)
	_, plain, err := ks.MakeKeyPair()
	if err != nil {
		t.Fatal(err)
	}
	if err := repo.SetPrivKey("blog.example.org", plain); err != nil {
		t.Fatal(err)
	}

	if err := MigrateEncryptedPrivKeys(cfg, repo, shared.NewLogger(io.Discard, "Error")); err != nil {
		t.Fatalf("MigrateEncryptedPrivKeys: %v", err)
	}

	stored, err := repo.GetPrivKey("blog.example.org")
	if err != nil {
		t.Fatal(err)
	}
	if stored != plain {
		t.Error("a plain key was rewritten")
	}
}

func TestMigrateEncryptedPrivKeysFailsOnWrongPassphrase(t *testing.T) {
	encrypted, _ := encryptedKey(t, "a different passphrase")
	cfg, repo := migRepo(t, map[string]string{"blog.example.org": encrypted})

	err := MigrateEncryptedPrivKeys(cfg, repo, shared.NewLogger(io.Discard, "Error"))
	if err == nil {
		t.Fatal("MigrateEncryptedPrivKeys succeeded with the wrong passphrase")
	}
	// The key is left as it was: storing what the wrong passphrase decrypted
	// to would destroy it, and the right passphrase may yet turn up.
	stored, serr := repo.GetPrivKey("blog.example.org")
	if serr != nil {
		t.Fatal(serr)
	}
	if stored != encrypted {
		t.Error("the key was overwritten although it could not be decrypted")
	}
}