Skip to content

Commit 5b37a63

Browse files
committed
Implement chpass command
1 parent 423ab20 commit 5b37a63

File tree

2 files changed

+171
-21
lines changed

2 files changed

+171
-21
lines changed

keyfile/keyfile.go

+67-20
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ type Argon2idParams struct {
2929
Memory uint32
3030
}
3131

32+
// Keyfields describes keyfile fields that must be preserved when a key is
33+
// reencrypted.
34+
type Keyfields struct {
35+
Comment string
36+
Fingerprint string
37+
}
38+
3239
// GenerateKeys generates a random Streamlined NTRU Prime 4591^761
3340
// public/secret key pair, writing the public key to pkw and secret key to skw.
3441
// The secret key is encrypted with ChaCha20-Poly1305 using a symmetric key
@@ -76,35 +83,68 @@ func GenerateKeys(rand io.Reader, pkw, skw io.Writer, passphrase []byte, kdfp *A
7683

7784
// Write secret key
7885
buf.Reset()
86+
kf := Keyfields{
87+
Comment: comment,
88+
Fingerprint: fingerprint,
89+
}
90+
err = writeSecretKey(buf, sk, kf, skKey, salt, time, memory, ncpu)
91+
if err != nil {
92+
return "", err
93+
}
94+
_, err = io.Copy(skw, buf)
95+
if err != nil {
96+
return "", err
97+
}
98+
99+
return fingerprint, nil
100+
}
101+
102+
func writeSecretKey(buf *bytes.Buffer, sk *SecretKey, kf Keyfields, skKey []byte, salt []byte, time, memory uint32, threads uint8) error {
79103
fmt.Fprintf(buf, "ss encryption secret key\n")
80-
fmt.Fprintf(buf, "comment: %s\n", comment)
104+
fmt.Fprintf(buf, "comment: %s\n", kf.Comment)
81105
fmt.Fprintf(buf, "cryptosystem: sntrup4591761\n")
82-
fmt.Fprintf(buf, "fingerprint: %s\n", fingerprint)
106+
fmt.Fprintf(buf, "fingerprint: %s\n", kf.Fingerprint)
83107
fmt.Fprintf(buf, "encryption: argon2id-chacha20-poly1305\n")
84108
fmt.Fprintf(buf, "argon2id-salt: %s\n", base64.StdEncoding.EncodeToString(salt))
85109
fmt.Fprintf(buf, "argon2id-time: %d\n", time)
86110
fmt.Fprintf(buf, "argon2id-memory: %d\n", memory)
87-
fmt.Fprintf(buf, "argon2id-threads: %d\n", ncpu)
111+
fmt.Fprintf(buf, "argon2id-threads: %d\n", threads)
88112
fmt.Fprintf(buf, "encoding: base64\n")
89113
// Everything above is Associated Data
90114
data := buf.Bytes()
91115
fmt.Fprintf(buf, "\n")
92116
aead, err := chacha20poly1305.New(skKey)
93117
if err != nil {
94-
return "", err
118+
return err
95119
}
96120
nonce := make([]byte, aead.NonceSize())
97121
skCiphertext := aead.Seal(nil, nonce, sk[:], data)
98-
enc = base64.NewEncoder(base64.StdEncoding, buf)
122+
enc := base64.NewEncoder(base64.StdEncoding, buf)
99123
enc.Write(skCiphertext)
100124
enc.Close()
101125
fmt.Fprintf(buf, "\n")
102-
_, err = io.Copy(skw, buf)
126+
return nil
127+
}
128+
129+
// EncryptSecretKey writes the secret key encrypted in keyfile format to skw.
130+
func EncryptSecretKey(rand io.Reader, skw io.Writer, sk *SecretKey, passphrase []byte, kdfp *Argon2idParams, kf Keyfields) error {
131+
salt := make([]byte, saltsize)
132+
_, err := rand.Read(salt)
103133
if err != nil {
104-
return "", err
134+
return err
105135
}
136+
ncpu := uint8(runtime.NumCPU())
137+
time := kdfp.Time
138+
memory := kdfp.Memory
139+
skKey := argon2.IDKey(passphrase, salt, time, memory, ncpu, chacha20poly1305.KeySize)
106140

107-
return fingerprint, nil
141+
buf := new(bytes.Buffer)
142+
err = writeSecretKey(buf, sk, kf, skKey, salt, time, memory, ncpu)
143+
if err != nil {
144+
return err
145+
}
146+
_, err = io.Copy(skw, buf)
147+
return err
108148
}
109149

110150
func readKeyFile(r io.Reader, firstLine string) (fields map[string]string, ad []byte, encodedKey string, err error) {
@@ -200,53 +240,60 @@ func ReadPublicKey(r io.Reader) (*PublicKey, error) {
200240

201241
// OpenSecretKey reads and decrypts an encryted Streamlined NTRU Prime 4591^761
202242
// secret key in the keyfile format from r.
203-
func OpenSecretKey(r io.Reader, passphrase []byte) (*SecretKey, error) {
243+
func OpenSecretKey(r io.Reader, passphrase []byte) (_ *SecretKey, _ Keyfields, err error) {
244+
e := func(err error) (*SecretKey, Keyfields, error) {
245+
return nil, Keyfields{}, err
246+
}
247+
204248
fields, keyAD, encodedSealedKey, err := readKeyFile(r, "ss encryption secret key")
205249
if err != nil {
206-
return nil, err
250+
return
207251
}
208252
sealedKey, err := base64.StdEncoding.DecodeString(encodedSealedKey)
209253
if err != nil {
210-
return nil, err
254+
return
211255
}
212256
err = requireFields(fields, map[string]string{
213257
"cryptosystem": "sntrup4591761",
214258
"encryption": "argon2id-chacha20-poly1305",
215259
"encoding": "base64",
216260
})
217261
if err != nil {
218-
return nil, err
262+
return
219263
}
220264
salt, err := base64.StdEncoding.DecodeString(fields["argon2id-salt"])
221265
if err != nil {
222-
return nil, err
266+
return
223267
}
224268
time, err := strconv.ParseUint(fields["argon2id-time"], 10, 32)
225269
if err != nil {
226-
return nil, fmt.Errorf("argon2id-time: %w", err)
270+
return e(fmt.Errorf("argon2id-time: %w", err))
227271
}
228272
memory, err := strconv.ParseUint(fields["argon2id-memory"], 10, 32)
229273
if err != nil {
230-
return nil, fmt.Errorf("argon2id-memory: %w", err)
274+
return e(fmt.Errorf("argon2id-memory: %w", err))
231275
}
232276
ncpu, err := strconv.ParseUint(fields["argon2id-threads"], 10, 8)
233277
if err != nil {
234-
return nil, fmt.Errorf("argon2id-threads: %w", err)
278+
return e(fmt.Errorf("argon2id-threads: %w", err))
235279
}
236280
derivedKey := argon2.IDKey(passphrase, salt, uint32(time), uint32(memory), uint8(ncpu), chacha20poly1305.KeySize)
237281
aead, err := chacha20poly1305.New(derivedKey)
238282
if err != nil {
239-
return nil, err
283+
return
240284
}
241285
skNonce := make([]byte, aead.NonceSize())
242286
key, err := aead.Open(sealedKey[:0], skNonce, sealedKey, keyAD)
243287
if err != nil {
244-
return nil, err
288+
return
245289
}
246290
sk := new(SecretKey)
247291
if len(key) != len(sk) {
248-
return nil, fmt.Errorf("secret key has invalid length %d", len(key))
292+
return e(fmt.Errorf("secret key has invalid length %d", len(key)))
249293
}
250294
copy(sk[:], key)
251-
return sk, nil
295+
var kf Keyfields
296+
kf.Comment = fields["comment"]
297+
kf.Fingerprint = fields["fingerprint"]
298+
return sk, kf, nil
252299
}

ss.go

+104-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"flag"
1111
"fmt"
1212
"io"
13+
"io/ioutil"
1314
"log"
1415
"os"
1516
"os/user"
@@ -24,6 +25,7 @@ import (
2425
func usage() {
2526
fmt.Fprintf(os.Stderr, `Usage of %s:
2627
%[1]s keygen [-i id] [-t time] [-m memory (MiB)] [-c comment]
28+
%[1]s chpass [-i id] [-t time] [-m memory (MiB)]
2729
%[1]s encrypt [-i id|pubkey] [-in input] [-out output]
2830
%[1]s encrypt -passphrase [-in input] [-out output] [-t time] [-m memory (MiB)]
2931
%[1]s decrypt [-i id] [-in input] [-out output]
@@ -45,6 +47,9 @@ func main() {
4547
case "keygen":
4648
fs := new(keygenFlags).parse(os.Args[2:])
4749
err = keygen(fs)
50+
case "chpass":
51+
fs := new(chpassFlags).parse(os.Args[2:])
52+
err = chpass(fs)
4853
case "encrypt":
4954
fs := new(encryptFlags).parse(os.Args[2:])
5055
encrypt(fs)
@@ -187,6 +192,104 @@ func keygen(fs *keygenFlags) (err error) {
187192
return nil
188193
}
189194

195+
type chpassFlags struct {
196+
identity string
197+
time uint
198+
memory uint
199+
force bool
200+
comment string
201+
}
202+
203+
func (f *chpassFlags) parse(args []string) *chpassFlags {
204+
fs := flag.NewFlagSet("ss chpass", flag.ExitOnError)
205+
fs.StringVar(&f.identity, "i", defaultID, "identity name")
206+
fs.UintVar(&f.time, "t", defaultTime, "Argon2id time")
207+
fs.UintVar(&f.memory, "m", defaultMemory, "Argon2id memory (MiB)")
208+
fs.BoolVar(&f.force, "f", false, "force Argon2id key derivation despite low parameters")
209+
fs.StringVar(&f.comment, "c", "", "comment")
210+
fs.Parse(args)
211+
return f
212+
}
213+
214+
func chpass(fs *chpassFlags) error {
215+
id := fs.identity
216+
appdir := appdir()
217+
skFilename := filepath.Join(appdir, id+".secret")
218+
if _, err := os.Stat(skFilename); os.IsNotExist(err) {
219+
return fmt.Errorf("%q not found", skFilename)
220+
}
221+
skFile, err := os.Open(skFilename)
222+
if err != nil {
223+
log.Fatal(err)
224+
}
225+
226+
time := uint32(fs.time)
227+
memory := uint32(fs.memory)
228+
if memory < defaultMemory {
229+
log.Printf("warning: recommended Argon2id memory parameter is %d MiB",
230+
defaultMemory)
231+
if !fs.force {
232+
return errors.New("choose stronger parameters, use defaults, or force with -f")
233+
}
234+
}
235+
236+
prompt := fmt.Sprintf("Current passphrase for %s", skFilename)
237+
passphrase, err := promptPassphrase(prompt)
238+
if err != nil {
239+
return err
240+
}
241+
if len(passphrase) == 0 {
242+
return errors.New("empty passphrase")
243+
}
244+
245+
sk, kf, err := keyfile.OpenSecretKey(skFile, passphrase)
246+
if err != nil {
247+
log.Printf("%s: %v", skFilename, err)
248+
log.Fatal("The secret keyfile cannot be opened. " +
249+
"This may be due to keyfile tampering or an incorrect passphrase.")
250+
}
251+
skFile.Close()
252+
253+
prompt = "New passphrase"
254+
passphrase, err = promptPassphrase(prompt)
255+
if err != nil {
256+
return err
257+
}
258+
if len(passphrase) == 0 {
259+
return errors.New("empty passphrase")
260+
}
261+
prompt += " (again)"
262+
passphraseAgain, err := promptPassphrase(prompt)
263+
if err != nil {
264+
return err
265+
}
266+
if !bytes.Equal(passphrase, passphraseAgain) {
267+
return errors.New("passphrases do not match")
268+
}
269+
270+
tmpDir, tmpBasename := filepath.Split(skFilename)
271+
tmpFi, err := ioutil.TempFile(tmpDir, tmpBasename)
272+
if err != nil {
273+
return err
274+
}
275+
err = tmpFi.Chmod(0600)
276+
if err != nil {
277+
return err
278+
}
279+
kdfp := &keyfile.Argon2idParams{Time: time, Memory: memory * 1024}
280+
err = keyfile.EncryptSecretKey(rand.Reader, tmpFi, sk, passphrase, kdfp, kf)
281+
if err != nil {
282+
return err
283+
}
284+
tmpFi.Close()
285+
err = os.Rename(tmpFi.Name(), skFilename)
286+
if err != nil {
287+
return err
288+
}
289+
log.Printf("rewrite %v", skFilename)
290+
return nil
291+
}
292+
190293
type encryptFlags struct {
191294
passphrase bool
192295
time uint
@@ -343,7 +446,7 @@ func decrypt(fs *decryptFlags) {
343446
if err != nil {
344447
log.Fatal(err)
345448
}
346-
sk, err := keyfile.OpenSecretKey(skFile, passphrase)
449+
sk, _, err := keyfile.OpenSecretKey(skFile, passphrase)
347450
if err != nil {
348451
log.Printf("%s: %v", skFilename, err)
349452
log.Fatal("The secret keyfile cannot be opened. " +

0 commit comments

Comments
 (0)