/
mnemonikey.go
248 lines (217 loc) · 8.87 KB
/
mnemonikey.go
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
package mnemonikey
import (
"bytes"
"errors"
"fmt"
"io"
"time"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"github.com/kklash/mnemonikey/pgp"
)
// SubkeyType represents a flavor of subkey, either encryption, authentication, or signing.
type SubkeyType string
const (
SubkeyTypeEncryption SubkeyType = "encryption"
SubkeyTypeAuthentication SubkeyType = "authentication"
SubkeyTypeSigning SubkeyType = "signing"
)
// ErrTTLInvalid is returned when constructing a Mnemonikey, if the TTL
// selected is less than zero.
var ErrTTLInvalid = errors.New("key time-to-live cannot be negative")
// ErrCreationTooLate is returned when constructing a Mnemonikey, if its creation
// time is too far in the future to fit in CreationOffsetBitCount.
var ErrCreationTooLate = errors.New("key creation time exceeds maximum")
// ErrCreationTooEarly is returned when constructing a Mnemonikey, if its creation
// time is before EpochStart.
var ErrCreationTooEarly = errors.New("key creation time exceeds maximum")
// Mnemonikey represents a determinstically generated set of PGP keys. It contains
// a master certification key, and encryption, authentication, and signing subkeys,
// as well as the seed data used to derive all four keys.
type Mnemonikey struct {
pgpKeySet *pgp.KeySet
seed *Seed
keyCreationTime time.Time
}
// New constructs a Mnemonikey from a seed.
//
// The key creation timestamp is hashed when computing the PGP public key fingerprint,
// and thus is critical to ensuring deterministic key re-generation. This function rounds
// the creation time down to the next lowest EpochIncrement before creation, so that it can
// be encoded into a recovery mnemonic.
//
// The user ID parameters, name and email, are not required but are highly recommended
// to assist in identifying the key later.
func New(seed *Seed, creation time.Time, opts *KeyOptions) (*Mnemonikey, error) {
if err := seed.Era().check(); err != nil {
return nil, err
}
if opts == nil {
opts = new(KeyOptions)
}
if opts.TTL < 0 {
return nil, ErrTTLInvalid
} else if creation.After(MaxCreationTime) {
return nil, ErrCreationTooLate
} else if creation.Before(EpochStart) {
return nil, ErrCreationTooEarly
}
// floor creation to next lowest EpochIncrement after EpochStart
creationOffset := creation.Sub(EpochStart) / EpochIncrement
creation = EpochStart.Add(EpochIncrement * creationOffset)
pgpKeySet, err := derivePGPKeySet(seed, creation, opts)
if err != nil {
return nil, err
}
mnk := &Mnemonikey{
seed: seed,
keyCreationTime: creation,
pgpKeySet: pgpKeySet,
}
return mnk, nil
}
// CreatedAt returns the key creation timestamp, rounded to an EpochIncrement
// after the EpochStart date.
func (mnk *Mnemonikey) CreatedAt() time.Time {
return mnk.keyCreationTime
}
// Expiry returns the key expiry time.
func (mnk *Mnemonikey) Expiry() time.Time {
return mnk.pgpKeySet.MasterKey.Expiry
}
// UserID returns the string form of the key's user ID.
func (mnk *Mnemonikey) UserID() string {
return string(mnk.pgpKeySet.UserID.Encode())
}
// Fingerprint returns the fingerprint hash of the master key.
func (mnk *Mnemonikey) Fingerprint() []byte {
return mnk.pgpKeySet.MasterKey.FingerprintV4()
}
// SubkeyTypes returns the types of subkeys available to the Mnemonikey.
func (mnk *Mnemonikey) SubkeyTypes() []SubkeyType {
subkeyTypes := make([]SubkeyType, 0, 3)
if mnk.pgpKeySet.EncryptionSubkey != nil {
subkeyTypes = append(subkeyTypes, SubkeyTypeEncryption)
}
if mnk.pgpKeySet.AuthenticationSubkey != nil {
subkeyTypes = append(subkeyTypes, SubkeyTypeAuthentication)
}
if mnk.pgpKeySet.SigningSubkey != nil {
subkeyTypes = append(subkeyTypes, SubkeyTypeSigning)
}
return subkeyTypes
}
// SubkeyFingerprint returns the fingerprint hash of the given subkey type.
// Returns nil if the Mnemonikey was created without the given subkey.
func (mnk *Mnemonikey) SubkeyFingerprint(subkeyType SubkeyType) []byte {
switch subkeyType {
case SubkeyTypeEncryption:
if mnk.pgpKeySet.EncryptionSubkey != nil {
return mnk.pgpKeySet.EncryptionSubkey.FingerprintV4()
}
case SubkeyTypeAuthentication:
if mnk.pgpKeySet.AuthenticationSubkey != nil {
return mnk.pgpKeySet.AuthenticationSubkey.FingerprintV4()
}
case SubkeyTypeSigning:
if mnk.pgpKeySet.SigningSubkey != nil {
return mnk.pgpKeySet.SigningSubkey.FingerprintV4()
}
}
return nil
}
// EncodePGP encodes the entire Mnemonikey as a series of binary OpenPGP packets.
//
// If password is provided, it is used to encrypt private key material with
// the OpenPGP String-to-Key algorithm.
func (mnk *Mnemonikey) EncodePGP(password []byte) ([]byte, error) {
return mnk.pgpKeySet.EncodePackets(password)
}
// EncodeSubkeysPGP encodes the Mnemonikey as a series of binary OpenPGP packets,
// but only includes the private key material for subkeys. The master key is
// encoded as a private key stub without providing the private key material itself.
//
// If withSelfCert is false, the self-certification signature by the master key
// will not be provided. Typically the self-certification is required when first importing
// a set of PGP keys into a keychain, and can be skipped if importing into a keychain which
// already has a self-certification signature recorded for the key. Leave withSelfCert as
// true if you are unsure.
//
// If password is provided, it is used to encrypt private key material with
// the OpenPGP String-to-Key algorithm.
func (mnk *Mnemonikey) EncodeSubkeysPGP(password []byte, withSelfCert bool) ([]byte, error) {
return mnk.pgpKeySet.EncodeSubkeyPackets(password, withSelfCert)
}
// EncodePGPArmor encodes the entire Mnemonikey as a series of OpenPGP packets
// and formats them to ASCII armor block format.
//
// If password is provided, it is used to encrypt private key material with
// the OpenPGP String-to-Key algorithm.
func (mnk *Mnemonikey) EncodePGPArmor(password []byte) (string, error) {
keyPacketData, err := mnk.pgpKeySet.EncodePackets(password)
if err != nil {
return "", err
}
pgpArmorKey, err := armorEncode(openpgp.PrivateKeyType, keyPacketData)
if err != nil {
return "", err
}
return pgpArmorKey, nil
}
// EncodeSubkeysPGPArmor encodes the Mnemonikey as a series of OpenPGP packets
// formatted to ASCII armor block format, but only includes the private key
// material for subkeys. The master key is encoded as a private key stub
// without providing the private key material itself.
//
// If withSelfCert is false, the self-certification signature by the master key
// will not be provided. Typically the self-certification is required when first importing
// a set of PGP keys into a keychain, and can be skipped if importing into a keychain which
// already has a self-certification signature recorded for the key. Leave withSelfCert as
// true if you are unsure.
//
// If password is provided, it is used to encrypt private key material with
// the OpenPGP String-to-Key algorithm.
func (mnk *Mnemonikey) EncodeSubkeysPGPArmor(password []byte, withSelfCert bool) (string, error) {
keyPacketData, err := mnk.EncodeSubkeysPGP(password, withSelfCert)
if err != nil {
return "", err
}
pgpArmorKey, err := armorEncode(openpgp.PrivateKeyType, keyPacketData)
if err != nil {
return "", err
}
return pgpArmorKey, nil
}
// creationOffset returns the creation offset number, used for encoding the recovery phrase.
func (mnk *Mnemonikey) creationOffset() uint32 {
return uint32(mnk.keyCreationTime.Sub(EpochStart) / EpochIncrement)
}
// EncodeMnemonicPlaintext encodes the Mnemonikey seed and creation offset into an English mnemonic
// recovery phrase. The recovery phrase alone is sufficient to recover the entire set of keys.
func (mnk *Mnemonikey) EncodeMnemonicPlaintext() ([]string, error) {
return EncodeMnemonicPlaintext(mnk.seed, mnk.creationOffset())
}
// EncodeMnemonicEncrypted encodes the Mnemonikey seed and creation offset into an English
// mnemonic recovery phrase. The recovery phrase is encrypted with the given password
// so that the same password must be used upon recovery to decrypt the phrase.
//
// Without the password, someone in possession of an encrypted phrase would see the key's
// metadata (version, creation time) but would not be able to use it to derive the correct
// PGP private keys.
func (mnk *Mnemonikey) EncodeMnemonicEncrypted(password []byte, random io.Reader) ([]string, error) {
return EncodeMnemonicEncrypted(mnk.seed, mnk.creationOffset(), password, random)
}
func armorEncode(blockType string, data []byte) (string, error) {
buf := new(bytes.Buffer)
armorWriter, err := armor.Encode(buf, blockType, nil)
if err != nil {
return "", fmt.Errorf("failed to construct armor encoder: %w", err)
}
if _, err := armorWriter.Write(data); err != nil {
return "", fmt.Errorf("failed to write PGP packets to armor encoder: %w", err)
}
if err := armorWriter.Close(); err != nil {
return "", fmt.Errorf("failed to close PGP armor encoder: %w", err)
}
return buf.String(), nil
}