1package kvstore
2
3import (
4	"encoding/base64"
5	"fmt"
6
7	"github.com/keybase/client/go/chat/signencrypt"
8	"github.com/keybase/client/go/kbcrypto"
9	"github.com/keybase/client/go/libkb"
10	"github.com/keybase/client/go/msgpack"
11	"github.com/keybase/client/go/protocol/keybase1"
12	"github.com/keybase/go-crypto/ed25519"
13)
14
15type KVStoreBoxer interface {
16	Box(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, cleartextValue string) (ciphertext string,
17		teamKeyGen keybase1.PerTeamKeyGeneration, ciphertextVersion int, err error)
18	Unbox(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, ciphertext string, teamKeyGen keybase1.PerTeamKeyGeneration, formatVersion int,
19		senderUID keybase1.UID, senderEldestSeqno keybase1.Seqno, senderDeviceID keybase1.DeviceID) (cleartext string, err error)
20}
21
22var _ KVStoreBoxer = (*KVStoreRealBoxer)(nil)
23
24type KVStoreRealBoxer struct {
25	libkb.Contextified
26}
27
28func NewKVStoreBoxer(g *libkb.GlobalContext) *KVStoreRealBoxer {
29	return &KVStoreRealBoxer{
30		Contextified: libkb.NewContextified(g),
31	}
32}
33
34type kvStoreMetadata struct {
35	EntryID           keybase1.KVEntryID `codec:"e" json:"e"`
36	Revision          int                `codec:"r" json:"r"`
37	EncKey            keybase1.Bytes32   `codec:"k" json:"k"`
38	CiphertextVersion int                `codec:"v" json:"v"`
39	UID               keybase1.UID       `codec:"u" json:"u"`
40	EldestSeqno       keybase1.Seqno     `codec:"s" json:"s"`
41	DeviceID          keybase1.DeviceID  `codec:"d" json:"d"`
42}
43
44func newNonce() (ret [signencrypt.NonceSize]byte, err error) {
45	randBytes, err := libkb.RandBytes(signencrypt.NonceSize)
46	if err != nil {
47		return ret, err
48	}
49	copy(ret[:], randBytes)
50	return ret, nil
51}
52
53func (b *KVStoreRealBoxer) fetchEncryptionKey(mctx libkb.MetaContext, entryID keybase1.KVEntryID, generation *keybase1.PerTeamKeyGeneration) (res [signencrypt.SecretboxKeySize]byte, gen keybase1.PerTeamKeyGeneration, err error) {
54	// boxing can always use the latest key, unboxing will pass in a team generation to load
55	loadArg := keybase1.FastTeamLoadArg{
56		ID:           entryID.TeamID,
57		Applications: []keybase1.TeamApplication{keybase1.TeamApplication_KVSTORE},
58	}
59	if generation == nil {
60		loadArg.NeedLatestKey = true
61	} else {
62		loadArg.KeyGenerationsNeeded = []keybase1.PerTeamKeyGeneration{*generation}
63	}
64	teamLoadRes, err := mctx.G().GetFastTeamLoader().Load(mctx, loadArg)
65	if err != nil {
66		return res, gen, err
67	}
68	if len(teamLoadRes.ApplicationKeys) != 1 {
69		return res, gen, fmt.Errorf("wrong number of keys from fast-team-loading encryption key; wanted 1, got %d", len(teamLoadRes.ApplicationKeys))
70	}
71	appKey := teamLoadRes.ApplicationKeys[0]
72	if generation != nil && appKey.KeyGeneration != *generation {
73		return res, gen, fmt.Errorf("wrong app key generation; wanted %d but got %d", *generation, appKey.KeyGeneration)
74	}
75	if appKey.Application != keybase1.TeamApplication_KVSTORE {
76		return res, gen, fmt.Errorf("wrong app key application; wanted %d but got %d", keybase1.TeamApplication_KVSTORE, appKey.Application)
77	}
78	var encKey [signencrypt.SecretboxKeySize]byte = appKey.Key
79	return encKey, appKey.KeyGeneration, nil
80}
81
82func (b *KVStoreRealBoxer) fetchVerifyKey(mctx libkb.MetaContext, uid keybase1.UID, deviceID keybase1.DeviceID) (ret signencrypt.VerifyKey, err error) {
83	upk, err := b.G().GetUPAKLoader().LoadUPAKWithDeviceID(mctx.Ctx(), uid, deviceID)
84	if err != nil {
85		return nil, err
86	}
87	verifyKID, _ := upk.Current.FindSigningDeviceKID(deviceID)
88	verifyKey := kbcrypto.KIDToNaclSigningKeyPublic(verifyKID.ToBytes())
89	if verifyKey == nil {
90		return nil, kbcrypto.BadKeyError{}
91	}
92	var verKey [ed25519.PublicKeySize]byte = *verifyKey
93	return &verKey, nil
94}
95
96func (b *KVStoreRealBoxer) Box(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, cleartext string) (
97	ciphertext string, teamKeyGen keybase1.PerTeamKeyGeneration, version int, err error) {
98
99	defer mctx.Trace(fmt.Sprintf("KVStoreRealBoxer#Box: %s, %s, %s", entryID.TeamID, entryID.Namespace, entryID.EntryKey),
100		&err)()
101
102	clearBytes := []byte(cleartext)
103	ciphertextVersion := 1
104	nonce, err := newNonce()
105	if err != nil {
106		mctx.Debug("error making a nonce: %v", err)
107		return "", keybase1.PerTeamKeyGeneration(0), 0, err
108	}
109	// get encryption key (and team generation) and this device's signing key
110	encKey, teamGen, err := b.fetchEncryptionKey(mctx, entryID, nil)
111	if err != nil {
112		mctx.Debug("error fetching encryption key for entry %+v: %v", entryID, err)
113		return "", keybase1.PerTeamKeyGeneration(0), 0, err
114	}
115	uv, deviceID, _, signingKey, _ := mctx.G().ActiveDevice.AllFields()
116	signingKP, ok := signingKey.(libkb.NaclSigningKeyPair)
117	if !ok || signingKP.Private == nil {
118		mctx.Debug("error with signing key: %v", err)
119		return "", keybase1.PerTeamKeyGeneration(0), 0, libkb.KeyCannotSignError{}
120	}
121	var signKey [ed25519.PrivateKeySize]byte = *signingKP.Private
122	// build associated data
123	associatedData := kvStoreMetadata{
124		EntryID:           entryID,
125		Revision:          revision,
126		EncKey:            encKey,
127		CiphertextVersion: ciphertextVersion,
128		UID:               uv.Uid,
129		EldestSeqno:       uv.EldestSeqno,
130		DeviceID:          deviceID,
131	}
132
133	// seal it all up
134	signEncryptedBytes, err := signencrypt.SealWithAssociatedData(
135		clearBytes, associatedData, &encKey, &signKey, kbcrypto.SignaturePrefixTeamStore, &nonce)
136	if err != nil {
137		mctx.Debug("error sealing message and associated data: %v", err)
138		return "", keybase1.PerTeamKeyGeneration(0), 0, err
139	}
140	boxed := keybase1.EncryptedKVEntry{
141		V: 1,
142		E: signEncryptedBytes,
143		N: nonce[:],
144	}
145	// pack it, string it, ship it.
146	packed, err := msgpack.Encode(boxed)
147	if err != nil {
148		mctx.Debug("error msgpacking secretbox for entry %+v: %v", entryID, err)
149		return "", keybase1.PerTeamKeyGeneration(0), 0, err
150	}
151	return base64.StdEncoding.EncodeToString(packed), teamGen, ciphertextVersion, nil
152}
153
154func (b *KVStoreRealBoxer) Unbox(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, ciphertext string,
155	teamKeyGen keybase1.PerTeamKeyGeneration, formatVersion int, senderUID keybase1.UID, senderEldestSeqno keybase1.Seqno,
156	senderDeviceID keybase1.DeviceID) (cleartext string, err error) {
157
158	defer mctx.Trace(fmt.Sprintf("KVStoreRealBoxer#Unbox: t:%s, n:%s, k:%s", entryID.TeamID, entryID.Namespace, entryID.EntryKey),
159		&err)()
160
161	if formatVersion != 1 {
162		return "", fmt.Errorf("unsupported format version %d isn't 1", formatVersion)
163	}
164	// basic decoding into a not-yet-unsealed box
165	decoded, err := base64.StdEncoding.DecodeString(ciphertext)
166	if err != nil {
167		mctx.Debug("boxed message isn't base64: %v", err)
168		return "", err
169	}
170	var box keybase1.EncryptedKVEntry
171	err = msgpack.Decode(&box, decoded)
172	if err != nil {
173		mctx.Debug("msgpack decode error on boxed message: %v", err)
174		return "", err
175	}
176	if box.V != 1 {
177		return "", fmt.Errorf("unsupported secret box version: %v", box.V)
178	}
179	// fetch encryption and verification keys
180	encKey, _, err := b.fetchEncryptionKey(mctx, entryID, &teamKeyGen)
181	if err != nil {
182		mctx.Debug("error fetching decryption key: %v", err)
183		return "", err
184	}
185	verKey, err := b.fetchVerifyKey(mctx, senderUID, senderDeviceID)
186	if err != nil {
187		mctx.Debug("error fetching verify key: %v", err)
188		return "", err
189	}
190	var nonce [signencrypt.NonceSize]byte
191	if copy(nonce[:], box.N) != signencrypt.NonceSize {
192		return "", libkb.DecryptBadNonceError{}
193	}
194	associatedData := kvStoreMetadata{
195		EntryID:           entryID,
196		Revision:          revision,
197		EncKey:            encKey,
198		CiphertextVersion: box.V,
199		UID:               senderUID,
200		EldestSeqno:       senderEldestSeqno,
201		DeviceID:          senderDeviceID,
202	}
203
204	// open it up
205	clearBytes, err := signencrypt.OpenWithAssociatedData(box.E, associatedData, &encKey, verKey, kbcrypto.SignaturePrefixTeamStore, &nonce)
206	if err != nil {
207		return "", err
208	}
209	return string(clearBytes), nil
210}
211