/
keystore.go
222 lines (189 loc) · 5.69 KB
/
keystore.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
package keystore
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/filefilego/filefilego/common"
"github.com/filefilego/filefilego/common/hexutil"
"github.com/golang-jwt/jwt/v5"
)
const jwtValidityHours = 2160
// KeyLockUnlockLister is an interface with locking, unlocking and listing key functionality.
type KeyLockUnlockLister interface {
LockKey(address string, jwt string) (bool, error)
UnlockKey(address string, passphrase string) (string, error)
ListKeys() ([]string, error)
KeyAuthorizer
}
// KeyAuthorizer is an interface with auth mechanism of a key.
type KeyAuthorizer interface {
Authorized(jwtToken string) (bool, UnlockedKey, error)
}
// UnlockedKey represents an unlocked key with a jwt token.
type UnlockedKey struct {
Key *Key
JWT string
}
// Store handles keypair storage.
type Store struct {
keysDir string
nodeIdentityData []byte
unlockedKeys map[string]UnlockedKey
mu sync.RWMutex
}
// New creates a new keystore.
func New(keysDir string, nodeIdentityData []byte) (*Store, error) {
if keysDir == "" {
return nil, errors.New("keysDir is empty")
}
if len(nodeIdentityData) == 0 {
return nil, errors.New("nodeIdentityData is empty")
}
return &Store{
keysDir: keysDir,
nodeIdentityData: nodeIdentityData,
unlockedKeys: make(map[string]UnlockedKey),
}, nil
}
// CreateKey generates a new key.
func (ks *Store) CreateKey(passphrase string) (string, error) {
if passphrase == "" {
return "", errors.New("passphrase is empty")
}
key, err := NewKey()
if err != nil {
return "", fmt.Errorf("failed to create key: %w", err)
}
fileName, err := ks.SaveKey(key, passphrase)
if err != nil {
return "", err
}
return fileName, nil
}
// SaveKey saves a key given the passphrase.
func (ks *Store) SaveKey(key *Key, passphrase string) (string, error) {
if passphrase == "" {
return "", errors.New("passphrase is empty")
}
keyDataJSON, err := key.MarshalToJSON(passphrase)
if err != nil {
return "", fmt.Errorf("failed to marshal key: %w", err)
}
fileName, err := common.WriteToFile(keyDataJSON, filepath.Join(ks.keysDir, generateFilename(key.KeyPair.Address)))
if err != nil {
return "", fmt.Errorf("failed to write key to file: %v", err)
}
return fileName, nil
}
// LockKey removes the key from unlocked keys.
func (ks *Store) LockKey(address string, jwt string) (bool, error) {
ks.mu.Lock()
defer ks.mu.Unlock()
acc, ok := ks.unlockedKeys[address]
if ok && acc.JWT == jwt {
delete(ks.unlockedKeys, address)
return true, nil
}
return false, fmt.Errorf("address %s not found", address)
}
// UnlockKey unlocks a key by address.
// it will try to unlock the node_identity_key first and if not then it will proceed with the keystore dir.
func (ks *Store) UnlockKey(address string, passphrase string) (string, error) {
ks.mu.Lock()
defer ks.mu.Unlock()
dirEntries, err := os.ReadDir(ks.keysDir)
if err != nil {
return "", fmt.Errorf("failed to read keystore directory: %w", err)
}
for _, entry := range dirEntries {
nodeIDKey := false
if entry.Name() == "node_identity.json" {
nodeIDKey = true
fileData, err := os.ReadFile(filepath.Join(ks.keysDir, entry.Name()))
if err != nil {
continue
}
nodeIDAddress := hexutil.ExtractHex(string(fileData))
if nodeIDAddress == "" {
continue
}
} else {
nodeIDKey = false
fileNameContainsAddress := strings.Contains(entry.Name(), address)
if !fileNameContainsAddress {
continue
}
}
bts, err := os.ReadFile(filepath.Join(ks.keysDir, entry.Name()))
if err != nil {
return "", fmt.Errorf("failed to read keystore file: %w", err)
}
key, err := UnmarshalKey(bts, passphrase)
if nodeIDKey && err != nil {
continue
}
if err != nil {
return "", fmt.Errorf("failed to unmarshal keystore file: %w", err)
}
atClaims := jwt.MapClaims{}
atClaims["address"] = key.Address
atClaims["exp"] = time.Now().Add(time.Hour * jwtValidityHours).Unix()
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
token, err := at.SignedString(ks.nodeIdentityData)
if err != nil {
return "", fmt.Errorf("failed to sign jwt token with node's identity file: %w", err)
}
ks.unlockedKeys[key.Address] = UnlockedKey{
Key: key,
JWT: token,
}
return token, nil
}
return "", errors.New("key not found on this node")
}
// ListKeys lists the keys in the keysDir.
func (ks *Store) ListKeys() ([]string, error) {
addresses := make([]string, 0)
dirEntries, err := os.ReadDir(ks.keysDir)
if err != nil {
return nil, fmt.Errorf("failed to read keystore directory: %w", err)
}
for _, entry := range dirEntries {
fileData, err := os.ReadFile(filepath.Join(ks.keysDir, entry.Name()))
if err != nil {
continue
}
address := hexutil.ExtractHex(string(fileData))
addresses = append(addresses, address)
}
return addresses, nil
}
// Authorized checks if a token is authorized and valid.
func (ks *Store) Authorized(jwtToken string) (bool, UnlockedKey, error) {
token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return ks.nodeIdentityData, nil
})
if err != nil {
return false, UnlockedKey{}, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return false, UnlockedKey{}, errors.New("token is invalid")
}
addr, addressFoundInJWT := claims["address"].(string)
if !addressFoundInJWT {
return false, UnlockedKey{}, errors.New("failed to extract address from jwt")
}
foundKey, found := ks.unlockedKeys[addr]
if !found {
return false, foundKey, errors.New("address is not unlocked")
}
return true, foundKey, nil
}