1package identity
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "time"
9
10 "github.com/ProtonMail/go-crypto/openpgp"
11 "github.com/ProtonMail/go-crypto/openpgp/armor"
12 "github.com/ProtonMail/go-crypto/openpgp/packet"
13 "github.com/pkg/errors"
14
15 "github.com/git-bug/git-bug/repository"
16)
17
18var errNoPrivateKey = fmt.Errorf("no private key")
19
20type Key struct {
21 public *packet.PublicKey
22 entity *openpgp.Entity
23}
24
25type keyConfig struct {
26 time time.Time
27}
28
29type KeyOption func(*keyConfig)
30
31// WithTime sets a specific time for key generation.
32// Useful for testing to ensure consistent, deterministic keys.
33func WithTime(t time.Time) KeyOption {
34 return func(cfg *keyConfig) {
35 cfg.time = t
36 }
37}
38
39// GenerateKey generate a key pair (public+private) with identity metadata.
40// The type and configuration of the key is determined by the default value in go's OpenPGP.
41func GenerateKey(id Interface, opts ...KeyOption) *Key {
42 cfg := &keyConfig{
43 time: time.Now(),
44 }
45 for _, opt := range opts {
46 opt(cfg)
47 }
48
49 entity, err := openpgp.NewEntity(id.Name(), id.Login(), id.Email(), &packet.Config{
50 Time: func() time.Time {
51 return cfg.time
52 },
53 })
54 if err != nil {
55 panic(err)
56 }
57
58 return &Key{
59 public: entity.PrimaryKey,
60 entity: entity,
61 }
62}
63
64// generatePublicKey generate only a public key (only useful for testing)
65// See GenerateKey for the details.
66func generatePublicKey(id Interface) *Key {
67 k := GenerateKey(id, WithTime(time.Time{}))
68 // k.entity = nil
69 k.entity.PrivateKey = nil
70 return k
71}
72
73func (k *Key) Public() *packet.PublicKey {
74 return k.entity.PrimaryKey
75}
76
77func (k *Key) Version() string {
78 return "new"
79}
80
81func (k *Key) Private() *packet.PrivateKey {
82 return k.entity.PrivateKey
83}
84
85func (k *Key) Validate() error {
86 if k.public == nil {
87 return fmt.Errorf("nil public key")
88 }
89 if !k.public.CanSign() {
90 return fmt.Errorf("public key can't sign")
91 }
92
93 if k.entity != nil && k.entity.PrivateKey != nil {
94 if !k.entity.PrivateKey.CanSign() {
95 return fmt.Errorf("private key can't sign")
96 }
97 }
98
99 return nil
100}
101
102func (k *Key) Clone() *Key {
103 clone := &Key{}
104
105 pub := *k.public
106 clone.public = &pub
107
108 if k.entity != nil {
109 entity := *k.entity
110 clone.entity = &entity
111 }
112
113 return clone
114}
115
116func (k *Key) MarshalJSON() ([]byte, error) {
117 // Serialize only the public key, in the armored format.
118 var buf bytes.Buffer
119 w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil)
120 if err != nil {
121 return nil, err
122 }
123
124 err = k.entity.Serialize(w)
125 if err != nil {
126 return nil, err
127 }
128 err = w.Close()
129 if err != nil {
130 return nil, err
131 }
132 return json.Marshal(buf.String())
133}
134
135func (k *Key) UnmarshalJSON(data []byte) error {
136 // De-serialize the entity using the armored format.
137 var armored string
138 err := json.Unmarshal(data, &armored)
139 if err != nil {
140 return err
141 }
142
143 entities, err := openpgp.ReadArmoredKeyRing(strings.NewReader(armored))
144 if err != nil {
145 return errors.Wrap(err, "failed to read armored key ring")
146 }
147
148 if len(entities) != 1 {
149 return fmt.Errorf("exactly one entity should be present - got %d", len(entities))
150 }
151
152 entity := entities[0]
153
154 // The armored format (RFC 4880) doesn't preserve key creation timestamps.
155 // We don't care about the creation time, so we reset it to the zero value to ensure consistency.
156 entity.PrimaryKey.CreationTime = time.Time{}
157
158 k.public = entity.PrimaryKey
159 k.entity = entity
160 return nil
161}
162
163func (k *Key) loadPrivate(repo repository.RepoKeyring) error {
164 item, err := repo.Keyring().Get(k.public.KeyIdString())
165 if err == repository.ErrKeyringKeyNotFound {
166 return errNoPrivateKey
167 }
168 if err != nil {
169 return err
170 }
171
172 entities, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(item.Data))
173 if err != nil {
174 return err
175 }
176
177 if len(entities) != 1 {
178 return fmt.Errorf("examtly one entity should be stored - got %d", len(entities))
179 }
180
181 // The armored format (RFC 4880) doesn't preserve key creation timestamps.
182 // We don't care about the creation time, so we reset it to the zero value to ensure consistency.
183 entities[0].PrivateKey.CreationTime = time.Time{}
184 k.entity = entities[0]
185 k.public = entities[0].PrimaryKey
186
187 return nil
188}
189
190// ensurePrivateKey attempt to load the corresponding private key if it is not loaded already.
191// If no private key is found, returns errNoPrivateKey
192func (k *Key) ensurePrivateKey(repo repository.RepoKeyring) error {
193 if k.entity != nil && k.entity.PrivateKey != nil {
194 return nil
195 }
196
197 return k.loadPrivate(repo)
198}
199
200func (k *Key) storePrivate(repo repository.RepoKeyring) error {
201 var buf bytes.Buffer
202 w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil)
203 if err != nil {
204 return err
205 }
206 err = k.entity.SerializePrivate(w, nil)
207 if err != nil {
208 return err
209 }
210 err = w.Close()
211 if err != nil {
212 return err
213 }
214
215 return repo.Keyring().Set(repository.Item{
216 Key: k.entity.PrimaryKey.KeyIdString(),
217 Data: buf.Bytes(),
218 })
219}
220
221func (k *Key) PGPEntity() *openpgp.Entity {
222 return k.entity
223}