waku-org/go-waku

View on GitHub
waku/v2/protocol/rln/keystore/keystore.go

Summary

Maintainability
A
2 hrs
Test Coverage
F
0%
package keystore

import (
    "bytes"
    "encoding/hex"
    "encoding/json"
    "errors"
    "fmt"
    "os"
    "strings"

    "github.com/ethereum/go-ethereum/accounts/keystore"
    "github.com/waku-org/go-waku/waku/v2/hash"
    "github.com/waku-org/go-zerokit-rln/rln"
    "go.uber.org/zap"
)

// New creates a new instance of a rln credentials keystore
func New(path string, appInfo AppInfo, logger *zap.Logger) (*AppKeystore, error) {
    logger = logger.Named("rln-keystore")

    _, err := os.Stat(path)
    if err != nil {
        if os.IsNotExist(err) {
            // If no keystore exists at path we create a new empty one with passed keystore parameters
            err = createAppKeystore(path, appInfo, defaultSeparator)
            if err != nil {
                return nil, err
            }
        } else {
            return nil, err
        }
    }

    src, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }

    for _, keystoreBytes := range bytes.Split(src, []byte(defaultSeparator)) {
        if len(keystoreBytes) == 0 {
            continue
        }

        keystore := new(AppKeystore)
        err := json.Unmarshal(keystoreBytes, keystore)
        if err != nil {
            continue
        }

        keystore.logger = logger
        keystore.path = path

        if keystore.Credentials == nil {
            keystore.Credentials = map[Key]appKeystoreCredential{}
        }

        if keystore.AppIdentifier == appInfo.AppIdentifier && keystore.Application == appInfo.Application && keystore.Version == appInfo.Version {
            return keystore, nil
        }
    }

    return nil, errors.New("no keystore found")
}

func getKey(treeIndex rln.MembershipIndex, filterMembershipContract MembershipContractInfo) (Key, error) {
    keyStr := fmt.Sprintf("%s%s%d", filterMembershipContract.ChainID, filterMembershipContract.Address, treeIndex)
    hash := hash.SHA256([]byte(keyStr))
    return Key(strings.ToUpper(hex.EncodeToString(hash))), nil
}

// GetMembershipCredentials decrypts and retrieves membership credentials from the keystore applying filters
func (k *AppKeystore) GetMembershipCredentials(keystorePassword string, index *rln.MembershipIndex, filterMembershipContract MembershipContractInfo) (*MembershipCredentials, error) {
    // If there is only one, and index to laod nil, assume 0,
    // if there is more than one, complain if the index to load is nil

    var key Key
    var err error

    if len(k.Credentials) == 0 {
        return nil, nil
    }

    if len(k.Credentials) == 1 {
        // Only one credential, the tree index does not matter.
        k.logger.Warn("automatically loading the only credential found on the keystore")
        for k := range k.Credentials {
            key = k // Obtain the first c
            break
        }
    } else {
        treeIndex := uint(0)
        if index != nil {
            treeIndex = *index
        } else {
            return nil, errors.New("the index of the onchain commitment to use was not specified")
        }

        key, err = getKey(treeIndex, filterMembershipContract)
        if err != nil {
            return nil, err
        }
    }

    credential, ok := k.Credentials[key]
    if !ok {
        return nil, nil
    }

    credentialsBytes, err := keystore.DecryptDataV3(credential.Crypto, keystorePassword)
    if err != nil {
        return nil, err
    }

    credentials := new(MembershipCredentials)
    err = json.Unmarshal(credentialsBytes, credentials)
    if err != nil {
        return nil, err
    }

    return credentials, nil
}

// AddMembershipCredentials inserts a membership credential to the keystore matching the application, appIdentifier and version filters.
func (k *AppKeystore) AddMembershipCredentials(newCredential MembershipCredentials, password string) error {
    credentials, err := k.GetMembershipCredentials(password, &newCredential.TreeIndex, newCredential.MembershipContractInfo)
    if err != nil {
        return err
    }

    key, err := getKey(newCredential.TreeIndex, newCredential.MembershipContractInfo)
    if err != nil {
        return err
    }

    if credentials != nil && credentials.TreeIndex == newCredential.TreeIndex && credentials.MembershipContractInfo.Equals(newCredential.MembershipContractInfo) {
        return errors.New("credential already present")
    }

    b, err := json.Marshal(newCredential)
    if err != nil {
        return err
    }

    encryptedCredentials, err := keystore.EncryptDataV3(b, []byte(password), keystore.StandardScryptN, keystore.StandardScryptP)
    if err != nil {
        return err
    }

    k.Credentials[key] = appKeystoreCredential{Crypto: encryptedCredentials}

    return save(k, k.path)
}

func createAppKeystore(path string, appInfo AppInfo, separator string) error {
    if separator == "" {
        separator = defaultSeparator
    }

    keystore := AppKeystore{
        Application:   appInfo.Application,
        AppIdentifier: appInfo.AppIdentifier,
        Version:       appInfo.Version,
        Credentials:   make(map[Key]appKeystoreCredential),
    }

    b, err := json.Marshal(keystore)
    if err != nil {
        return err
    }

    b = append(b, []byte(separator)...)

    buffer := new(bytes.Buffer)

    err = json.Compact(buffer, b)
    if err != nil {
        return err
    }

    return os.WriteFile(path, buffer.Bytes(), 0600)
}

// Safely saves a Keystore's JsonNode to disk.
// If exists, the destination file is renamed with extension .bkp; the file is written at its destination and the .bkp file is removed if write is successful, otherwise is restored
func save(keystore *AppKeystore, path string) error {
    // We first backup the current keystore
    _, err := os.Stat(path)
    if err == nil {
        err := os.Rename(path, path+".bkp")
        if err != nil {
            return err
        }
    }

    b, err := json.Marshal(keystore)
    if err != nil {
        return err
    }

    b = append(b, []byte(defaultSeparator)...)

    buffer := new(bytes.Buffer)

    err = json.Compact(buffer, b)
    if err != nil {
        restoreErr := os.Rename(path, path+".bkp")
        if restoreErr != nil {
            return fmt.Errorf("could not restore backup file: %w", restoreErr)
        }
        return err
    }

    err = os.WriteFile(path, buffer.Bytes(), 0600)
    if err != nil {
        restoreErr := os.Rename(path, path+".bkp")
        if restoreErr != nil {
            return fmt.Errorf("could not restore backup file: %w", restoreErr)
        }
        return err
    }

    // The write went fine, so we can remove the backup keystore
    _, err = os.Stat(path + ".bkp")
    if err == nil {
        err := os.Remove(path + ".bkp")
        if err != nil {
            return err
        }
    }

    return nil
}