1package storage
2
3import (
4	"bytes"
5	"encoding/pem"
6	"fmt"
7	"io"
8	"io/ioutil"
9	"os"
10	"path/filepath"
11	"strings"
12
13	"github.com/sirupsen/logrus"
14	"github.com/theupdateframework/notary"
15)
16
17// NewFileStore creates a fully configurable file store
18func NewFileStore(baseDir, fileExt string) (*FilesystemStore, error) {
19	baseDir = filepath.Clean(baseDir)
20	if err := createDirectory(baseDir, notary.PrivExecPerms); err != nil {
21		return nil, err
22	}
23	if !strings.HasPrefix(fileExt, ".") {
24		fileExt = "." + fileExt
25	}
26
27	return &FilesystemStore{
28		baseDir: baseDir,
29		ext:     fileExt,
30	}, nil
31}
32
33// NewPrivateKeyFileStorage initializes a new filestore for private keys, appending
34// the notary.PrivDir to the baseDir.
35func NewPrivateKeyFileStorage(baseDir, fileExt string) (*FilesystemStore, error) {
36	baseDir = filepath.Join(baseDir, notary.PrivDir)
37	myStore, err := NewFileStore(baseDir, fileExt)
38	myStore.migrateTo0Dot4()
39	return myStore, err
40}
41
42// NewPrivateSimpleFileStore is a wrapper to create an owner readable/writeable
43// _only_ filestore
44func NewPrivateSimpleFileStore(baseDir, fileExt string) (*FilesystemStore, error) {
45	return NewFileStore(baseDir, fileExt)
46}
47
48// FilesystemStore is a store in a locally accessible directory
49type FilesystemStore struct {
50	baseDir string
51	ext     string
52}
53
54func (f *FilesystemStore) moveKeyTo0Dot4Location(file string) {
55	keyID := filepath.Base(file)
56	fileDir := filepath.Dir(file)
57	d, _ := f.Get(file)
58	block, _ := pem.Decode(d)
59	if block == nil {
60		logrus.Warn("Key data for", file, "could not be decoded as a valid PEM block. The key will not been migrated and may not be available")
61		return
62	}
63	fileDir = strings.TrimPrefix(fileDir, notary.RootKeysSubdir)
64	fileDir = strings.TrimPrefix(fileDir, notary.NonRootKeysSubdir)
65	if fileDir != "" {
66		block.Headers["gun"] = filepath.ToSlash(fileDir[1:])
67	}
68	if strings.Contains(keyID, "_") {
69		role := strings.Split(keyID, "_")[1]
70		keyID = strings.TrimSuffix(keyID, "_"+role)
71		block.Headers["role"] = role
72	}
73	var keyPEM bytes.Buffer
74	// since block came from decoding the PEM bytes in the first place, and all we're doing is adding some headers we ignore the possibility of an error while encoding the block
75	pem.Encode(&keyPEM, block)
76	f.Set(keyID, keyPEM.Bytes())
77}
78
79func (f *FilesystemStore) migrateTo0Dot4() {
80	rootKeysSubDir := filepath.Clean(filepath.Join(f.Location(), notary.RootKeysSubdir))
81	nonRootKeysSubDir := filepath.Clean(filepath.Join(f.Location(), notary.NonRootKeysSubdir))
82	if _, err := os.Stat(rootKeysSubDir); !os.IsNotExist(err) && f.Location() != rootKeysSubDir {
83		if rootKeysSubDir == "" || rootKeysSubDir == "/" {
84			// making sure we don't remove a user's homedir
85			logrus.Warn("The directory for root keys is an unsafe value, we are not going to delete the directory. Please delete it manually")
86		} else {
87			// root_keys exists, migrate things from it
88			listOnlyRootKeysDirStore, _ := NewFileStore(rootKeysSubDir, f.ext)
89			for _, file := range listOnlyRootKeysDirStore.ListFiles() {
90				f.moveKeyTo0Dot4Location(filepath.Join(notary.RootKeysSubdir, file))
91			}
92			// delete the old directory
93			os.RemoveAll(rootKeysSubDir)
94		}
95	}
96
97	if _, err := os.Stat(nonRootKeysSubDir); !os.IsNotExist(err) && f.Location() != nonRootKeysSubDir {
98		if nonRootKeysSubDir == "" || nonRootKeysSubDir == "/" {
99			// making sure we don't remove a user's homedir
100			logrus.Warn("The directory for non root keys is an unsafe value, we are not going to delete the directory. Please delete it manually")
101		} else {
102			// tuf_keys exists, migrate things from it
103			listOnlyNonRootKeysDirStore, _ := NewFileStore(nonRootKeysSubDir, f.ext)
104			for _, file := range listOnlyNonRootKeysDirStore.ListFiles() {
105				f.moveKeyTo0Dot4Location(filepath.Join(notary.NonRootKeysSubdir, file))
106			}
107			// delete the old directory
108			os.RemoveAll(nonRootKeysSubDir)
109		}
110	}
111
112	// if we have a trusted_certificates folder, let's delete for a complete migration since it is unused by new clients
113	certsSubDir := filepath.Join(f.Location(), "trusted_certificates")
114	if certsSubDir == "" || certsSubDir == "/" {
115		logrus.Warn("The directory for trusted certificate is an unsafe value, we are not going to delete the directory. Please delete it manually")
116	} else {
117		os.RemoveAll(certsSubDir)
118	}
119}
120
121func (f *FilesystemStore) getPath(name string) (string, error) {
122	fileName := fmt.Sprintf("%s%s", name, f.ext)
123	fullPath := filepath.Join(f.baseDir, fileName)
124
125	if !strings.HasPrefix(fullPath, f.baseDir) {
126		return "", ErrPathOutsideStore
127	}
128	return fullPath, nil
129}
130
131// GetSized returns the meta for the given name (a role) up to size bytes
132// If size is "NoSizeLimit", this corresponds to "infinite," but we cut off at a
133// predefined threshold "notary.MaxDownloadSize". If the file is larger than size
134// we return ErrMaliciousServer for consistency with the HTTPStore
135func (f *FilesystemStore) GetSized(name string, size int64) ([]byte, error) {
136	p, err := f.getPath(name)
137	if err != nil {
138		return nil, err
139	}
140	file, err := os.OpenFile(p, os.O_RDONLY, notary.PrivNoExecPerms)
141	if err != nil {
142		if os.IsNotExist(err) {
143			err = ErrMetaNotFound{Resource: name}
144		}
145		return nil, err
146	}
147	defer file.Close()
148
149	if size == NoSizeLimit {
150		size = notary.MaxDownloadSize
151	}
152
153	stat, err := file.Stat()
154	if err != nil {
155		return nil, err
156	}
157	if stat.Size() > size {
158		return nil, ErrMaliciousServer{}
159	}
160
161	l := io.LimitReader(file, size)
162	return ioutil.ReadAll(l)
163}
164
165// Get returns the meta for the given name.
166func (f *FilesystemStore) Get(name string) ([]byte, error) {
167	p, err := f.getPath(name)
168	if err != nil {
169		return nil, err
170	}
171	meta, err := ioutil.ReadFile(p)
172	if err != nil {
173		if os.IsNotExist(err) {
174			err = ErrMetaNotFound{Resource: name}
175		}
176		return nil, err
177	}
178	return meta, nil
179}
180
181// SetMulti sets the metadata for multiple roles in one operation
182func (f *FilesystemStore) SetMulti(metas map[string][]byte) error {
183	for role, blob := range metas {
184		err := f.Set(role, blob)
185		if err != nil {
186			return err
187		}
188	}
189	return nil
190}
191
192// Set sets the meta for a single role
193func (f *FilesystemStore) Set(name string, meta []byte) error {
194	fp, err := f.getPath(name)
195	if err != nil {
196		return err
197	}
198
199	// Ensures the parent directories of the file we are about to write exist
200	err = os.MkdirAll(filepath.Dir(fp), notary.PrivExecPerms)
201	if err != nil {
202		return err
203	}
204
205	// if something already exists, just delete it and re-write it
206	os.RemoveAll(fp)
207
208	// Write the file to disk
209	return ioutil.WriteFile(fp, meta, notary.PrivNoExecPerms)
210}
211
212// RemoveAll clears the existing filestore by removing its base directory
213func (f *FilesystemStore) RemoveAll() error {
214	return os.RemoveAll(f.baseDir)
215}
216
217// Remove removes the metadata for a single role - if the metadata doesn't
218// exist, no error is returned
219func (f *FilesystemStore) Remove(name string) error {
220	p, err := f.getPath(name)
221	if err != nil {
222		return err
223	}
224	return os.RemoveAll(p) // RemoveAll succeeds if path doesn't exist
225}
226
227// Location returns a human readable name for the storage location
228func (f FilesystemStore) Location() string {
229	return f.baseDir
230}
231
232// ListFiles returns a list of all the filenames that can be used with Get*
233// to retrieve content from this filestore
234func (f FilesystemStore) ListFiles() []string {
235	files := make([]string, 0, 0)
236	filepath.Walk(f.baseDir, func(fp string, fi os.FileInfo, err error) error {
237		// If there are errors, ignore this particular file
238		if err != nil {
239			return nil
240		}
241		// Ignore if it is a directory
242		if fi.IsDir() {
243			return nil
244		}
245
246		// If this is a symlink, ignore it
247		if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
248			return nil
249		}
250
251		// Only allow matches that end with our certificate extension (e.g. *.crt)
252		matched, _ := filepath.Match("*"+f.ext, fi.Name())
253
254		if matched {
255			// Find the relative path for this file relative to the base path.
256			fp, err = filepath.Rel(f.baseDir, fp)
257			if err != nil {
258				return err
259			}
260			trimmed := strings.TrimSuffix(fp, f.ext)
261			files = append(files, trimmed)
262		}
263		return nil
264	})
265	return files
266}
267
268// createDirectory receives a string of the path to a directory.
269// It does not support passing files, so the caller has to remove
270// the filename by doing filepath.Dir(full_path_to_file)
271func createDirectory(dir string, perms os.FileMode) error {
272	// This prevents someone passing /path/to/dir and 'dir' not being created
273	// If two '//' exist, MkdirAll deals it with correctly
274	dir = dir + "/"
275	return os.MkdirAll(dir, perms)
276}
277