nuts-foundation/nuts-node

View on GitHub
crypto/storage/fs/fs.go

Summary

Maintainability
A
0 mins
Test Coverage
B
82%
/*
 * Nuts node
 * Copyright (C) 2021 Nuts community
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package fs

import (
    "context"
    "crypto"
    "errors"
    "fmt"
    "github.com/nuts-foundation/nuts-node/crypto/log"
    "io/fs"
    "os"
    "path/filepath"
    "strings"

    "github.com/nuts-foundation/nuts-node/core"
    "github.com/nuts-foundation/nuts-node/crypto/storage/spi"
    "github.com/nuts-foundation/nuts-node/crypto/util"
)

type entryType string

// StorageType is the name of this storage type, used in health check reports and configuration.
const StorageType = "fs"

const (
    privateKeyEntry entryType = "private.pem"
)

type fileOpenError struct {
    filePath string
    kid      string
    err      error
}

// Error returns the string representation
func (f *fileOpenError) Error() string {
    return fmt.Sprintf("could not open entry %s with filename %s: %v", f.kid, f.filePath, f.err)
}

// Unwrap is needed for fileOpenError to be UnWrapped
func (f *fileOpenError) Unwrap() error {
    return f.err
}

type fileSystemBackend struct {
    fspath string
}

func (fsc fileSystemBackend) Name() string {
    return StorageType
}

func (fsc fileSystemBackend) CheckHealth() map[string]core.Health {
    return map[string]core.Health{
        "filesystem": {Status: core.HealthStatusUp},
    }
}

// NewFileSystemBackend creates a new filesystem backend, all directories will be created for the given path
// Using a filesystem backend in production is not recommended!
func NewFileSystemBackend(fspath string) (spi.Storage, error) {
    if fspath == "" {
        return nil, errors.New("filesystem path is empty")
    }
    fsc := &fileSystemBackend{
        fspath,
    }

    // Assert base directory is present
    err := os.MkdirAll(fsc.fspath, 0700)
    if err != nil {
        return nil, err
    }

    return fsc, nil
}

func (fsc fileSystemBackend) NewPrivateKey(ctx context.Context, namingFunc func(crypto.PublicKey) (string, error)) (crypto.PublicKey, string, error) {
    return spi.GenerateAndStore(ctx, fsc, namingFunc)
}

func (fsc fileSystemBackend) PrivateKeyExists(_ context.Context, kid string) (bool, error) {
    _, err := os.Stat(fsc.getEntryPath(kid, privateKeyEntry))
    if errors.Is(err, os.ErrNotExist) {
        return false, nil
    }
    return err == nil, err
}

// GetPrivateKey loads the private key for the given legalEntity from disk. Since a legalEntity has a URI as identifier, the URI is base64 encoded and postfixed with '_private.pem'. Keys are stored in pem format and are 2k RSA keys.
func (fsc fileSystemBackend) GetPrivateKey(_ context.Context, kid string) (crypto.Signer, error) {
    data, err := fsc.readEntry(kid, privateKeyEntry)
    if err != nil {
        return nil, err
    }
    privateKey, err := util.PemToPrivateKey(data)
    if err != nil {
        return nil, err
    }
    return privateKey, nil
}

// SavePrivateKey saves the private key for the given key to disk. Files are postfixed with '_private.pem'. Keys are stored in pem format.
func (fsc fileSystemBackend) SavePrivateKey(_ context.Context, kid string, key crypto.PrivateKey) error {
    filenamePath := fsc.getEntryPath(kid, privateKeyEntry)
    outFile, err := os.OpenFile(filenamePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.FileMode(0600))

    if err != nil {
        return err
    }

    defer outFile.Close()

    pem, err := util.PrivateKeyToPem(key)
    if err != nil {
        return err
    }

    _, err = outFile.Write([]byte(pem))

    return err
}

// DeletePrivateKey removes the private key for the given legalEntity from disk.
func (fsc fileSystemBackend) DeletePrivateKey(_ context.Context, kid string) error {
    filePath := fsc.getEntryPath(kid, privateKeyEntry)
    err := os.Remove(filePath)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            return spi.ErrNotFound
        }
        return fmt.Errorf("could not open entry %s with filename %s: %w", kid, filePath, err)
    }
    return nil
}

func (fsc fileSystemBackend) ListPrivateKeys(_ context.Context) []string {
    var result []string
    err := filepath.Walk(fsc.fspath, func(path string, info fs.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !info.IsDir() && strings.HasSuffix(info.Name(), string(privateKeyEntry)) {
            upper := len(info.Name()) - len(privateKeyEntry) - 1
            if upper > 0 {
                result = append(result, info.Name()[:upper])
            }
        }
        return nil
    })
    if err != nil {
        log.Logger().
            WithError(err).
            Errorf("Error while listing private keys in %s", fsc.fspath)
    }
    return result
}

func (fsc fileSystemBackend) readEntry(kid string, entryType entryType) ([]byte, error) {
    filePath := fsc.getEntryPath(kid, entryType)
    data, err := os.ReadFile(filePath)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            return nil, &fileOpenError{kid: kid, filePath: filePath, err: spi.ErrNotFound}
        }
        return nil, &fileOpenError{kid: kid, filePath: filePath, err: err}
    }
    return data, nil
}

func (fsc fileSystemBackend) getEntryPath(kid string, entryType entryType) string {
    return filepath.Join(fsc.fspath, getEntryFileName(kid, entryType))
}

func getEntryFileName(kid string, entryType entryType) string {
    return fmt.Sprintf("%s_%s", kid, entryType)
}