~egtann/shh

ref: v1.9.0 shh/shh.go -rw-r--r-- 11.7 KiB
53855162 — Evan Tann update install instructions, add man page 4 months ago
                                                                                
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
)

var b64 = base64.StdEncoding

type Secret struct {
	// Value is the AES-GCM encrypted text appended to the random nonce. It
	// is base64 encoded.
	Value []byte `json:"value"`

	// Users maps usernames to encrypted passwords which, when decrypted
	// using RSA, are used to decrypt the Value.
	Users map[username][]byte `json:"users"`
}

// shhV1 describes the .shh file containing secrets and public keys.
type shhV1 struct {
	// Secrets maps secretName -> (secretValue, Users). Secret names and
	// values are shared project-wide, so there can only be one instance of
	// any secret, the AES key for which is encrypted for each user using
	// their respective RSA public keys.
	Secrets map[string]*Secret `json:"secrets"`

	// Keys are RSA public keys used to encrypt secrets for each user.
	Keys map[username]*pem.Block `json:"keys"`

	// Version of the shh file. Note that this is independent of the shh
	// binary's version.
	Version int `json:"version"`

	// path of the .shh file itself.
	path string
}

// shhV0 describes the legacy .shh file format containing secrets and public
// keys. This format was abandoned because:
//
// 1. There's a security vulnerability in that AES secrets are not
//    authenticated, which allows for padded-oracle attacks.
// 2. Data was needlessly duplicated in the data structure, then deduplicated
//    at runtime. As a result of this change, `namespace` is no longer needed
//    in shhV1.
// 3. The new structure allows for a much cleaner API to interact with the
//    data, deduplicating encryption/decryption logic and simplifying the code
//    throughout.
type shhV0 struct {
	// Secrets maps users -> secret_labels -> secret_value. Each secret is
	// uniquely encrypted for each user given their public key.
	Secrets map[username]map[string]aesSecret `json:"secrets"`

	// Keys are public keys used to encrypt secrets for each user.
	Keys map[username]*pem.Block `json:"keys"`

	// namespace to which all secret names are added. This prevents two
	// users creating their own secrets which have the same name but
	// resolve to different secrets.
	namespace map[string]struct{}

	// path of the .shh file itself.
	path string
}

type aesSecret struct {
	AESKey    string `json:"key"`
	Encrypted string `json:"value"`
}

func newShh(path string) *shhV1 {
	return &shhV1{
		Secrets: map[string]*Secret{},
		Keys:    map[username]*pem.Block{},
		Version: 1,
		path:    path,
	}
}

// findFileRecursive checks for a file recursively up the filesystem until it
// hits an error.
func findFileRecursive(pth string) (string, error) {
	abs, err := filepath.Abs(pth)
	if err != nil {
		return "", fmt.Errorf("abs: %w", err)
	}
	if abs == string(filepath.Separator)+filepath.Base(pth) {
		// We hit the root, we're done
		return "", os.ErrNotExist
	}
	_, err = os.Stat(pth)
	switch {
	case os.IsNotExist(err):
		return findFileRecursive(filepath.Join("..", pth))
	case err != nil:
		return "", fmt.Errorf("stat: %w", err)
	}
	return pth, nil
}

func shhFromPath(pth string) (*shhV1, error) {
	recursivePath, err := findFileRecursive(pth)
	switch {
	case err == os.ErrNotExist:
		err = nil // Ignore error, keep going
	case err != nil:
		return nil, err
	}
	if recursivePath != "" {
		pth = recursivePath
	}
	flags := os.O_CREATE | os.O_RDWR
	fi, err := os.OpenFile(pth, flags, 0644)
	if err != nil {
		return nil, err
	}
	defer fi.Close()
	shh := newShh(pth)
	dec := json.NewDecoder(fi)
	err = dec.Decode(shh)
	switch {
	case err == io.EOF:
		// We newly created the file. Not an error, just an empty .shh
		return shh, nil
	case err != nil:
		return nil, fmt.Errorf("decode: %w", err)
	}
	return shh, nil
}

func (s *shhV1) EncodeToFile() error {
	flags := os.O_TRUNC | os.O_CREATE | os.O_WRONLY
	fi, err := os.OpenFile(s.path, flags, 0644)
	if err != nil {
		return err
	}
	defer fi.Close()
	return s.Encode(fi)
}

func (s *shhV1) Encode(w io.Writer) error {
	enc := json.NewEncoder(w)
	enc.SetIndent("", "\t")
	return enc.Encode(s)
}

// decrypt a secret returning plaintext.
func (s *Secret) decrypt(
	u username,
	privKey *rsa.PrivateKey,
) (string, error) {
	// Ensure we check that the user has access to the file in the first
	// place
	base64AESEncKey, ok := s.Users[u]
	if !ok {
		return "", errors.New("no access")
	}

	tmp, err := b64.DecodeString(string(base64AESEncKey))
	if err != nil {
		return "", fmt.Errorf("base64: %w", err)
	}
	aesEncKey := []byte(tmp)

	// Decrypt the user's private id_rsa key with the provided password,
	// then use the RSA private key to decrypt the AES password.
	key, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privKey,
		aesEncKey, nil)
	if err != nil {
		return "", fmt.Errorf("oaep: %w", err)
	}

	// Decrypt the secret Value using the AES password with GCM, which
	// offers Authenticated Encryption. This follows the example in Go's
	// stdlib:
	// https://godoc.org/crypto/cipher#NewGCM
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	ciphertext, err := b64.DecodeString(string(s.Value))
	if err != nil {
		return "", fmt.Errorf("base64: %w", err)
	}
	nonceSize := aesgcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return "", errors.New("encrypted secret too short")
	}
	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return "", fmt.Errorf("aes open: %w", err)
	}
	return string(plaintext), nil
}

// encrypt for all users.
func (s *Secret) encrypt(
	plaintext string,
	pubKeys map[username]*pem.Block,
) error {
	// Generate an AES key to encrypt the data. We use AES-256 which
	// requires a 32-byte key. We make a new key each time we encrypt
	// secrets to remove the risk of a nonce collision. This AES code
	// follows the example in Go's stdlib:
	//
	// https://godoc.org/crypto/cipher#NewGCM
	key := make([]byte, 32)
	if _, err := rand.Read(key); err != nil {
		return err
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return err
	}
	nonce := make([]byte, 12)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return err
	}
	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return err
	}
	ciphertext := aesgcm.Seal(nil, nonce, []byte(plaintext), nil)
	s.Value = []byte(b64.EncodeToString(append(nonce, ciphertext...)))

	// Reencrypt the AES key for each user using their own RSA key. We use
	// OAEP per the recommendation of the Go stdlib docs:
	//
	//	The original specification for encryption and signatures with
	//	RSA is PKCS#1 and the terms "RSA encryption" and "RSA
	//	signatures" by default refer to PKCS#1 version 1.5. However,
	//	that specification has flaws and new designs should use version
	//	two, usually called by just OAEP and PSS, where possible.
	//
	// https://golang.org/pkg/crypto/rsa/
	for u := range s.Users {
		// Encrypt the AES key using the public key
		pubKey, err := x509.ParsePKCS1PublicKey(pubKeys[u].Bytes)
		if err != nil {
			return fmt.Errorf("parse public key: %w", err)
		}
		encAESKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader,
			pubKey, key, nil)
		if err != nil {
			return fmt.Errorf("oaep: %w", err)
		}
		s.Users[u] = []byte(b64.EncodeToString(encAESKey))
	}
	return nil
}

// secretsForGlob returns all secret names which match a glob pattern in O(n).
func (s *shhV1) secretsForGlob(globPattern string) []*Secret {
	var secrets []*Secret
	for secretName, secret := range s.Secrets {
		if glob(globPattern, secretName) {
			secrets = append(secrets, secret)
		}
	}
	return secrets
}

// migrateShh from any previous versions to the lastest version.
func migrateShh(filename string) error {
	byt, err := ioutil.ReadFile(filename)
	if err != nil {
		return fmt.Errorf("read file: %w", err)
	}
	data := map[string]interface{}{}
	if err := json.Unmarshal(byt, &data); err != nil {
		return fmt.Errorf("unmarshal: %w", err)
	}
	val, ok := data["version"]
	if !ok {
		// Default to version 0
		val = float64(0)
	}
	fltVersion, ok := val.(float64)
	if !ok {
		return errors.New("bad version")
	}
	switch fltVersion {
	case 0:
		if err = migrateShhV0(filename, byt); err != nil {
			return fmt.Errorf("migrate v0: %w", err)
		}
		return nil
	case 1:
		// This is the current version. Nothing to do.
		return nil
	default:
		return errors.New("unknown version")
	}
}

// migrateShhV0 to v1. This is a one-time migration that moves from AES-CFB to
// AES-GCM (preventing Oracle Padding Attacks) and improves the data-structure
// of the underlying file to reduce filesize and improve performance of the
// most common shh operations.
func migrateShhV0(filename string, byt []byte) error {
	fmt.Println("performing a one-time migration of .shh from v0 to v1")
	global, project, err := getConfigPaths()
	if err != nil {
		return err
	}
	conf, err := configFromPaths(global, project)
	if err != nil {
		return err
	}
	self, err := getUser(conf)
	if err != nil {
		return fmt.Errorf("get user: %w", err)
	}
	self.Password, err = requestPassword(self.Port, defaultPasswordPrompt)
	if err != nil {
		return err
	}
	self.Keys, err = getKeys(global, self.Password)
	if err != nil {
		return err
	}

	fi, err := os.Open(filename)
	if err != nil {
		return err
	}

	shhOld := &shhV0{}
	if json.NewDecoder(fi).Decode(shhOld); err != nil {
		return fmt.Errorf("decode: %w", err)
	}

	shhNew := &shhV1{
		Keys:    shhOld.Keys,
		Secrets: map[string]*Secret{},
		Version: 1,
		path:    filename,
	}

	// Report an error if the user running the migration doesn't have
	// access to every secret, since we're unable to convert the old
	// per-user encrypted form to the new, project-wide data structure.
	var secretCount int
	for _, oldSecrets := range shhOld.Secrets {
		if len(oldSecrets) > secretCount {
			secretCount = len(oldSecrets)
		}
	}
	if len(shhOld.Secrets[self.Username]) != secretCount {
		return errors.New("you do not have access to every secret, " +
			"so shh cannot perform a one-time security " +
			"migration automatically. ask for access (or " +
			"delete secrets to which you do not have access), " +
			"then re-run")
	}

	// Remap secrets from the old form to the new form:
	//
	// shhOld.Secrets map[username]map[secretName]aesSecret
	// to
	// shhNew.Secrets map[secretName]*Secret
	for secretName, aesSecret := range shhOld.Secrets[self.Username] {
		if _, ok := shhNew.Secrets[secretName]; !ok {
			shhNew.Secrets[secretName] = &Secret{
				Users: map[username][]byte{},
			}
		}
		for user, userSecrets := range shhOld.Secrets {
			if _, ok := userSecrets[secretName]; !ok {
				continue
			}
			shhNew.Secrets[secretName].Users[user] = []byte{}
		}

		// Decrypt the user-specific version using the AES key
		encAESKey, err := b64.DecodeString(string(aesSecret.AESKey))
		if err != nil {
			return fmt.Errorf("decode: %w", err)
		}
		key, err := rsa.DecryptOAEP(sha256.New(), rand.Reader,
			self.Keys.PrivateKey, []byte(encAESKey), nil)
		if err != nil {
			return fmt.Errorf("decrypt secret: %w", err)
		}
		block, err := aes.NewCipher(key)
		if err != nil {
			return err
		}
		aesEncSecret, err := b64.DecodeString(
			string(aesSecret.Encrypted))
		if err != nil {
			return fmt.Errorf("decode: %w", err)
		}
		ciphertext := []byte(aesEncSecret)
		iv := ciphertext[:aes.BlockSize]
		ciphertext = ciphertext[aes.BlockSize:]
		stream := cipher.NewCFBDecrypter(block, iv)
		plaintext := make([]byte, len(ciphertext))
		stream.XORKeyStream(plaintext, ciphertext)

		// ciphertext now contains the plaintext, since it was
		// decrypted in place.
		err = shhNew.Secrets[secretName].encrypt(string(plaintext),
			shhNew.Keys)
		if err != nil {
			return fmt.Errorf("encrypt: %w", err)
		}
	}

	if err = shhNew.EncodeToFile(); err != nil {
		return err
	}
	fmt.Println("done")
	return nil
}