1package identity
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io"
8 "strings"
9 "time"
10
11 "github.com/ProtonMail/go-crypto/openpgp"
12 "github.com/ProtonMail/go-crypto/openpgp/armor"
13 "github.com/ProtonMail/go-crypto/openpgp/packet"
14 "github.com/pkg/errors"
15
16 "github.com/MichaelMure/git-bug/repository"
17)
18
19var errNoPrivateKey = fmt.Errorf("no private key")
20
21type Key struct {
22 public *packet.PublicKey
23 private *packet.PrivateKey
24}
25
26// GenerateKey generate a keypair (public+private)
27// The type and configuration of the key is determined by the default value in go's OpenPGP.
28func GenerateKey() *Key {
29 entity, err := openpgp.NewEntity("", "", "", &packet.Config{
30 // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal.
31 // We don't care about the creation time so we can set it to the zero value.
32 Time: func() time.Time {
33 return time.Time{}
34 },
35 })
36 if err != nil {
37 panic(err)
38 }
39 return &Key{
40 public: entity.PrimaryKey,
41 private: entity.PrivateKey,
42 }
43}
44
45// generatePublicKey generate only a public key (only useful for testing)
46// See GenerateKey for the details.
47func generatePublicKey() *Key {
48 k := GenerateKey()
49 k.private = nil
50 return k
51}
52
53func (k *Key) Public() *packet.PublicKey {
54 return k.public
55}
56
57func (k *Key) Private() *packet.PrivateKey {
58 return k.private
59}
60
61func (k *Key) Validate() error {
62 if k.public == nil {
63 return fmt.Errorf("nil public key")
64 }
65 if !k.public.CanSign() {
66 return fmt.Errorf("public key can't sign")
67 }
68
69 if k.private != nil {
70 if !k.private.CanSign() {
71 return fmt.Errorf("private key can't sign")
72 }
73 }
74
75 return nil
76}
77
78func (k *Key) Clone() *Key {
79 clone := &Key{}
80
81 pub := *k.public
82 clone.public = &pub
83
84 if k.private != nil {
85 priv := *k.private
86 clone.private = &priv
87 }
88
89 return clone
90}
91
92func (k *Key) MarshalJSON() ([]byte, error) {
93 // Serialize only the public key, in the armored format.
94 var buf bytes.Buffer
95 w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil)
96 if err != nil {
97 return nil, err
98 }
99
100 err = k.public.Serialize(w)
101 if err != nil {
102 return nil, err
103 }
104 err = w.Close()
105 if err != nil {
106 return nil, err
107 }
108 return json.Marshal(buf.String())
109}
110
111func (k *Key) UnmarshalJSON(data []byte) error {
112 // De-serialize only the public key, in the armored format.
113 var armored string
114 err := json.Unmarshal(data, &armored)
115 if err != nil {
116 return err
117 }
118
119 block, err := armor.Decode(strings.NewReader(armored))
120 if err == io.EOF {
121 return fmt.Errorf("no armored data found")
122 }
123 if err != nil {
124 return err
125 }
126
127 if block.Type != openpgp.PublicKeyType {
128 return fmt.Errorf("invalid key type")
129 }
130
131 p, err := packet.Read(block.Body)
132 if err != nil {
133 return errors.Wrap(err, "failed to read public key packet")
134 }
135
136 public, ok := p.(*packet.PublicKey)
137 if !ok {
138 return errors.New("got no packet.publicKey")
139 }
140
141 // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal.
142 // We don't care about the creation time so we can set it to the zero value.
143 public.CreationTime = time.Time{}
144
145 k.public = public
146 return nil
147}
148
149func (k *Key) loadPrivate(repo repository.RepoKeyring) error {
150 item, err := repo.Keyring().Get(k.public.KeyIdString())
151 if err == repository.ErrKeyringKeyNotFound {
152 return errNoPrivateKey
153 }
154 if err != nil {
155 return err
156 }
157
158 block, err := armor.Decode(bytes.NewReader(item.Data))
159 if err == io.EOF {
160 return fmt.Errorf("no armored data found")
161 }
162 if err != nil {
163 return err
164 }
165
166 if block.Type != openpgp.PrivateKeyType {
167 return fmt.Errorf("invalid key type")
168 }
169
170 p, err := packet.Read(block.Body)
171 if err != nil {
172 return errors.Wrap(err, "failed to read private key packet")
173 }
174
175 private, ok := p.(*packet.PrivateKey)
176 if !ok {
177 return errors.New("got no packet.privateKey")
178 }
179
180 // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal.
181 // We don't care about the creation time so we can set it to the zero value.
182 private.CreationTime = time.Time{}
183
184 k.private = private
185 return nil
186}
187
188// ensurePrivateKey attempt to load the corresponding private key if it is not loaded already.
189// If no private key is found, returns errNoPrivateKey
190func (k *Key) ensurePrivateKey(repo repository.RepoKeyring) error {
191 if k.private != nil {
192 return nil
193 }
194
195 return k.loadPrivate(repo)
196}
197
198func (k *Key) storePrivate(repo repository.RepoKeyring) error {
199 var buf bytes.Buffer
200 w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil)
201 if err != nil {
202 return err
203 }
204 err = k.private.Serialize(w)
205 if err != nil {
206 return err
207 }
208 err = w.Close()
209 if err != nil {
210 return err
211 }
212
213 return repo.Keyring().Set(repository.Item{
214 Key: k.public.KeyIdString(),
215 Data: buf.Bytes(),
216 })
217}
218
219func (k *Key) PGPEntity() *openpgp.Entity {
220 uid := packet.NewUserId("", "", "")
221 return &openpgp.Entity{
222 PrimaryKey: k.public,
223 PrivateKey: k.private,
224 Identities: map[string]*openpgp.Identity{
225 uid.Id: {
226 Name: uid.Id,
227 UserId: uid,
228 SelfSignature: &packet.Signature{
229 IsPrimaryId: func() *bool { b := true; return &b }(),
230 },
231 },
232 },
233 }
234}