From 42c0e123d99affe751eb8bbcba178609fdff1ecb Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Wed, 5 Apr 2023 15:44:46 -0400 Subject: [PATCH] refactor: credentials --- examples/chat2/exec.go | 8 - waku/node.go | 2 - waku/node_no_rln.go | 4 - waku/node_rln.go | 19 +- waku/v2/node/rln-credentials.go | 113 ------ waku/v2/node/wakunode2_rln.go | 14 +- waku/v2/node/wakuoptions.go | 4 +- waku/v2/node/wakuoptions_rln.go | 19 +- waku/v2/protocol/rln/common.go | 6 - .../rln/group_manager/dynamic/dynamic.go | 136 ++++++- .../rln/group_manager/dynamic/web3.go | 44 +-- waku/v2/protocol/rln/keystore/keystore.go | 336 ++++++++++++++++++ waku/v2/protocol/rln/waku_rln_relay.go | 6 - 13 files changed, 487 insertions(+), 224 deletions(-) delete mode 100644 waku/v2/node/rln-credentials.go create mode 100644 waku/v2/protocol/rln/keystore/keystore.go diff --git a/examples/chat2/exec.go b/examples/chat2/exec.go index b929c599..2bb8ffb7 100644 --- a/examples/chat2/exec.go +++ b/examples/chat2/exec.go @@ -125,14 +125,6 @@ func execute(options Options) { return } - if options.RLNRelay.Enable && options.RLNRelay.Dynamic { - err := node.WriteRLNMembershipCredentialsToFile(wakuNode.RLNRelay().MembershipKeyPair(), wakuNode.RLNRelay().MembershipIndex(), wakuNode.RLNRelay().MembershipContractAddress(), options.RLNRelay.CredentialsPath, []byte(options.RLNRelay.CredentialsPassword)) - if err != nil { - fmt.Println(err.Error()) - return - } - } - chat := NewChat(ctx, wakuNode, options) p := tea.NewProgram(chat.ui) if err := p.Start(); err != nil { diff --git a/waku/node.go b/waku/node.go index f9e36797..27599909 100644 --- a/waku/node.go +++ b/waku/node.go @@ -331,8 +331,6 @@ func Execute(options Options) { } } - onStartRLN(wakuNode, options) - var rpcServer *rpc.WakuRpc if options.RPCServer.Enable { rpcServer = rpc.NewWakuRpc(wakuNode, options.RPCServer.Address, options.RPCServer.Port, options.RPCServer.Admin, options.RPCServer.Private, options.PProf, options.RPCServer.RelayCacheCapacity, logger) diff --git a/waku/node_no_rln.go b/waku/node_no_rln.go index 4357e8d9..a76349f9 100644 --- a/waku/node_no_rln.go +++ b/waku/node_no_rln.go @@ -11,7 +11,3 @@ import ( func checkForRLN(logger *zap.Logger, options Options, nodeOpts *[]node.WakuNodeOption) { // Do nothing } - -func onStartRLN(wakuNode *node.WakuNode, options Options) { - // Do nothing -} diff --git a/waku/node_rln.go b/waku/node_rln.go index 5002414f..8449c40b 100644 --- a/waku/node_rln.go +++ b/waku/node_rln.go @@ -25,19 +25,13 @@ func checkForRLN(logger *zap.Logger, options Options, nodeOpts *[]node.WakuNodeO if options.RLNRelay.ETHPrivateKey != nil { ethPrivKey = options.RLNRelay.ETHPrivateKey } - membershipCredentials, err := node.GetMembershipCredentials( - logger, - options.RLNRelay.CredentialsPath, - options.RLNRelay.CredentialsPassword, - options.RLNRelay.MembershipContractAddress, - uint(options.RLNRelay.MembershipIndex), - ) - failOnErr(err, "Invalid membership credentials") *nodeOpts = append(*nodeOpts, node.WithDynamicRLNRelay( options.RLNRelay.PubsubTopic, options.RLNRelay.ContentTopic, - membershipCredentials, + options.RLNRelay.CredentialsPath, + options.RLNRelay.CredentialsPassword, + options.RLNRelay.MembershipContractAddress, nil, options.RLNRelay.ETHClientAddress, ethPrivKey, @@ -46,10 +40,3 @@ func checkForRLN(logger *zap.Logger, options Options, nodeOpts *[]node.WakuNodeO } } } - -func onStartRLN(wakuNode *node.WakuNode, options Options) { - if options.RLNRelay.Enable && options.RLNRelay.Dynamic && options.RLNRelay.CredentialsPath != "" { - err := node.WriteRLNMembershipCredentialsToFile(wakuNode.RLNRelay().MembershipKeyPair(), wakuNode.RLNRelay().MembershipIndex(), wakuNode.RLNRelay().MembershipContractAddress(), options.RLNRelay.CredentialsPath, []byte(options.RLNRelay.CredentialsPassword)) - failOnErr(err, "Could not write membership credentials file") - } -} diff --git a/waku/v2/node/rln-credentials.go b/waku/v2/node/rln-credentials.go deleted file mode 100644 index 29adf3da..00000000 --- a/waku/v2/node/rln-credentials.go +++ /dev/null @@ -1,113 +0,0 @@ -//go:build gowaku_rln -// +build gowaku_rln - -package node - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/waku-org/go-zerokit-rln/rln" - "go.uber.org/zap" -) - -const RLN_CREDENTIALS_FILENAME = "rlnCredentials.txt" - -func WriteRLNMembershipCredentialsToFile(keyPair *rln.MembershipKeyPair, idx rln.MembershipIndex, contractAddress common.Address, path string, passwd []byte) error { - if path == "" { - return nil // we dont want to use a credentials file - } - - if keyPair == nil { - return nil // no credentials to store - } - - credentialsJSON, err := json.Marshal(MembershipCredentials{ - Keypair: keyPair, - Index: idx, - Contract: contractAddress, - }) - - if err != nil { - return err - } - - encryptedCredentials, err := keystore.EncryptDataV3(credentialsJSON, passwd, keystore.StandardScryptN, keystore.StandardScryptP) - if err != nil { - return err - } - - output, err := json.Marshal(encryptedCredentials) - if err != nil { - return err - } - - path = filepath.Join(path, RLN_CREDENTIALS_FILENAME) - - return ioutil.WriteFile(path, output, 0600) -} - -func loadMembershipCredentialsFromFile(credentialsFilePath string, passwd string) (MembershipCredentials, error) { - src, err := ioutil.ReadFile(credentialsFilePath) - if err != nil { - return MembershipCredentials{}, err - } - - var encryptedK keystore.CryptoJSON - err = json.Unmarshal(src, &encryptedK) - if err != nil { - return MembershipCredentials{}, err - } - - credentialsBytes, err := keystore.DecryptDataV3(encryptedK, passwd) - if err != nil { - return MembershipCredentials{}, err - } - - var credentials MembershipCredentials - err = json.Unmarshal(credentialsBytes, &credentials) - - return credentials, err -} - -func GetMembershipCredentials(logger *zap.Logger, credentialsPath string, password string, membershipContract common.Address, membershipIndex uint) (credentials MembershipCredentials, err error) { - if credentialsPath == "" { // Not using a file - return MembershipCredentials{ - Contract: membershipContract, - }, nil - } - - credentialsFilePath := filepath.Join(credentialsPath, RLN_CREDENTIALS_FILENAME) - if _, err = os.Stat(credentialsFilePath); err == nil { - if credentials, err := loadMembershipCredentialsFromFile(credentialsFilePath, password); err != nil { - return MembershipCredentials{}, fmt.Errorf("could not read membership credentials file: %w", err) - } else { - logger.Info("loaded rln credentials", zap.String("filepath", credentialsFilePath)) - if (bytes.Equal(credentials.Contract.Bytes(), common.Address{}.Bytes())) { - credentials.Contract = membershipContract - } - if (bytes.Equal(membershipContract.Bytes(), common.Address{}.Bytes())) { - return MembershipCredentials{}, errors.New("no contract address specified") - } - return credentials, nil - } - } - - if os.IsNotExist(err) { - return MembershipCredentials{ - Keypair: nil, - Index: membershipIndex, - Contract: membershipContract, - }, nil - - } - - return MembershipCredentials{}, fmt.Errorf("could not read membership credentials file: %w", err) -} diff --git a/waku/v2/node/wakunode2_rln.go b/waku/v2/node/wakunode2_rln.go index 846bae73..3ba3d6e9 100644 --- a/waku/v2/node/wakunode2_rln.go +++ b/waku/v2/node/wakunode2_rln.go @@ -58,21 +58,13 @@ func (w *WakuNode) mountRlnRelay(ctx context.Context) error { } else { w.log.Info("setting up waku-rln-relay in on-chain mode") - // check if the peer has provided its rln credentials - var memKeyPair *r.IdentityCredential - if w.opts.rlnRelayIDCommitment != nil && w.opts.rlnRelayIDKey != nil { - memKeyPair = &r.IdentityCredential{ - IDCommitment: *w.opts.rlnRelayIDCommitment, - IDSecretHash: *w.opts.rlnRelayIDKey, - } - } - groupManager, err = dynamic.NewDynamicGroupManager( w.opts.rlnETHClientAddress, w.opts.rlnETHPrivateKey, w.opts.rlnMembershipContractAddress, - memKeyPair, - w.opts.rlnRelayMemIndex, + w.opts.keystorePath, + w.opts.keystorePassword, + true, w.opts.rlnRegistrationHandler, w.log, ) diff --git a/waku/v2/node/wakuoptions.go b/waku/v2/node/wakuoptions.go index 9f42ce3d..b90668d9 100644 --- a/waku/v2/node/wakuoptions.go +++ b/waku/v2/node/wakuoptions.go @@ -100,10 +100,10 @@ type WakuNodeParameters struct { rlnRelayContentTopic string rlnRelayDynamic bool rlnSpamHandler func(message *pb.WakuMessage) error - rlnRelayIDKey *[32]byte - rlnRelayIDCommitment *[32]byte rlnETHPrivateKey *ecdsa.PrivateKey rlnETHClientAddress string + keystorePath string + keystorePassword string rlnMembershipContractAddress common.Address rlnRegistrationHandler func(tx *types.Transaction) diff --git a/waku/v2/node/wakuoptions_rln.go b/waku/v2/node/wakuoptions_rln.go index dfb96363..263a4f82 100644 --- a/waku/v2/node/wakuoptions_rln.go +++ b/waku/v2/node/wakuoptions_rln.go @@ -25,29 +25,20 @@ func WithStaticRLNRelay(pubsubTopic string, contentTopic string, memberIndex r.M } } -type MembershipCredentials struct { - Contract common.Address `json:"contract"` - Keypair *r.MembershipKeyPair `json:"membershipKeyPair"` - Index r.MembershipIndex `json:"rlnIndex"` -} - -// WithStaticRLNRelay enables the Waku V2 RLN protocol in onchain mode. +// WithDynamicRLNRelay enables the Waku V2 RLN protocol in onchain mode. // Requires the `gowaku_rln` build constrain (or the env variable RLN=true if building go-waku) -func WithDynamicRLNRelay(pubsubTopic string, contentTopic string, membershipCredentials MembershipCredentials, spamHandler rln.SpamHandler, ethClientAddress string, ethPrivateKey *ecdsa.PrivateKey, registrationHandler rln.RegistrationHandler) WakuNodeOption { +func WithDynamicRLNRelay(pubsubTopic string, contentTopic string, keystorePath string, keystorePassword string, membershipContract common.Address, spamHandler rln.SpamHandler, ethClientAddress string, ethPrivateKey *ecdsa.PrivateKey, registrationHandler rln.RegistrationHandler) WakuNodeOption { return func(params *WakuNodeParameters) error { params.enableRLN = true params.rlnRelayDynamic = true - params.rlnRelayMemIndex = membershipCredentials.Index - if membershipCredentials.Keypair != nil { - params.rlnRelayIDKey = &membershipCredentials.Keypair.IDKey - params.rlnRelayIDCommitment = &membershipCredentials.Keypair.IDCommitment - } + params.keystorePassword = keystorePassword + params.keystorePath = keystorePath params.rlnRelayPubsubTopic = pubsubTopic params.rlnRelayContentTopic = contentTopic params.rlnSpamHandler = spamHandler params.rlnETHClientAddress = ethClientAddress params.rlnETHPrivateKey = ethPrivateKey - params.rlnMembershipContractAddress = membershipCredentials.Contract + params.rlnMembershipContractAddress = membershipContract params.rlnRegistrationHandler = registrationHandler return nil } diff --git a/waku/v2/protocol/rln/common.go b/waku/v2/protocol/rln/common.go index 6633608d..79c6be06 100644 --- a/waku/v2/protocol/rln/common.go +++ b/waku/v2/protocol/rln/common.go @@ -24,12 +24,6 @@ const MAX_EPOCH_GAP = int64(MAX_CLOCK_GAP_SECONDS / rln.EPOCH_UNIT_SECONDS) // Acceptable roots for merkle root validation of incoming messages const AcceptableRootWindowSize = 5 -type AppInfo struct { - Application string - AppIdentifier string - Version string -} - type RegistrationHandler = func(tx *types.Transaction) type SpamHandler = func(message *pb.WakuMessage) error diff --git a/waku/v2/protocol/rln/group_manager/dynamic/dynamic.go b/waku/v2/protocol/rln/group_manager/dynamic/dynamic.go index eebc5bcd..e968fe99 100644 --- a/waku/v2/protocol/rln/group_manager/dynamic/dynamic.go +++ b/waku/v2/protocol/rln/group_manager/dynamic/dynamic.go @@ -4,17 +4,27 @@ import ( "context" "crypto/ecdsa" "errors" + "math/big" "sync" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" + "github.com/waku-org/go-waku/waku/v2/protocol/rln/contracts" "github.com/waku-org/go-waku/waku/v2/protocol/rln/group_manager" + "github.com/waku-org/go-waku/waku/v2/protocol/rln/keystore" "github.com/waku-org/go-zerokit-rln/rln" r "github.com/waku-org/go-zerokit-rln/rln" "go.uber.org/zap" ) +var RLNAppInfo = keystore.AppInfo{ + Application: "go-waku-rln-relay", + AppIdentifier: "01234567890abcdef", + Version: "0.1", +} + type DynamicGroupManager struct { rln *rln.RLN log *zap.Logger @@ -22,8 +32,9 @@ type DynamicGroupManager struct { cancel context.CancelFunc wg sync.WaitGroup - identityCredential *rln.IdentityCredential - membershipIndex *rln.MembershipIndex + identityCredential *rln.IdentityCredential + membershipIndex *rln.MembershipIndex + membershipContractAddress common.Address ethClientAddress string ethClient *ethclient.Client @@ -34,8 +45,15 @@ type DynamicGroupManager struct { ethAccountPrivateKey *ecdsa.PrivateKey registrationHandler RegistrationHandler + chainId *big.Int + rlnContract *contracts.RLN + membershipFee *big.Int lastIndexLoaded int64 + saveKeystore bool + keystorePath string + keystorePassword string + rootTracker *group_manager.MerkleRootTracker } @@ -45,22 +63,34 @@ func NewDynamicGroupManager( ethClientAddr string, ethAccountPrivateKey *ecdsa.PrivateKey, memContractAddr common.Address, - identityCredential *rln.IdentityCredential, - index rln.MembershipIndex, + keystorePath string, + keystorePassword string, + saveKeystore bool, registrationHandler RegistrationHandler, log *zap.Logger, ) (*DynamicGroupManager, error) { return &DynamicGroupManager{ - identityCredential: identityCredential, - membershipIndex: &index, membershipContractAddress: memContractAddr, ethClientAddress: ethClientAddr, ethAccountPrivateKey: ethAccountPrivateKey, registrationHandler: registrationHandler, lastIndexLoaded: -1, + saveKeystore: saveKeystore, + keystorePath: keystorePath, + keystorePassword: keystorePassword, }, nil } +func (gm *DynamicGroupManager) getMembershipFee(ctx context.Context) (*big.Int, error) { + auth, err := bind.NewKeyedTransactorWithChainID(gm.ethAccountPrivateKey, gm.chainId) + if err != nil { + return nil, err + } + auth.Context = ctx + + return gm.rlnContract.MEMBERSHIPDEPOSIT(&bind.CallOpts{Context: ctx}) +} + func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN, rootTracker *group_manager.MerkleRootTracker) error { if gm.cancel != nil { return errors.New("already started") @@ -71,14 +101,55 @@ func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN, gm.log.Info("mounting rln-relay in on-chain/dynamic mode") + backend, err := ethclient.Dial(gm.ethClientAddress) + if err != nil { + return err + } + gm.ethClient = backend + gm.rln = rlnInstance gm.rootTracker = rootTracker - err := rootTracker.Sync() + gm.chainId, err = backend.ChainID(ctx) if err != nil { return err } + gm.rlnContract, err = contracts.NewRLN(gm.membershipContractAddress, backend) + if err != nil { + return err + } + + // check if the contract exists by calling a static function + gm.membershipFee, err = gm.getMembershipFee(ctx) + if err != nil { + return err + } + + err = rootTracker.Sync() + if err != nil { + return err + } + + if gm.keystorePassword != "" && gm.keystorePath != "" { + credentials, err := keystore.GetMembershipCredentials(gm.log, + gm.keystorePath, + gm.keystorePassword, + RLNAppInfo, + nil, + []keystore.MembershipContract{{ + ChainId: gm.chainId.String(), + Address: gm.membershipContractAddress.Hex(), + }}) + if err != nil { + return err + } + + // TODO: accept an index from the config + gm.identityCredential = &credentials[0].IdentityCredential + gm.membershipIndex = &credentials[0].MembershipGroups[0].TreeIndex + } + // prepare rln membership key pair if gm.identityCredential == nil && gm.ethAccountPrivateKey != nil { gm.log.Debug("no rln-relay key is provided, generating one") @@ -90,16 +161,23 @@ func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN, gm.identityCredential = identityCredential // register the rln-relay peer to the membership contract - membershipIndex, err := gm.Register(ctx) + gm.membershipIndex, err = gm.Register(ctx) if err != nil { return err } - gm.membershipIndex = membershipIndex + err = gm.persistCredentials() + if err != nil { + return err + } gm.log.Info("registered peer into the membership contract") } + if gm.identityCredential == nil || gm.membershipIndex == nil { + return errors.New("no credentials available") + } + handler := func(pubkey r.IDCommitment, index r.MembershipIndex) error { return gm.InsertMember(pubkey) } @@ -115,6 +193,46 @@ func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN, return nil } +func (gm *DynamicGroupManager) persistCredentials() error { + if !gm.saveKeystore { + return nil + } + + if gm.identityCredential == nil || gm.membershipIndex == nil { + return errors.New("no credentials to persist") + } + + path := gm.keystorePath + if path == "" { + gm.log.Warn("keystore: no credentials path set, using default path", zap.String("path", keystore.RLN_CREDENTIALS_FILENAME)) + path = keystore.RLN_CREDENTIALS_FILENAME + } + + password := gm.keystorePassword + if password == "" { + gm.log.Warn("keystore: no credentials password set, using default password", zap.String("password", keystore.RLN_CREDENTIALS_PASSWORD)) + password = keystore.RLN_CREDENTIALS_PASSWORD + } + + keystoreCred := keystore.MembershipCredentials{ + IdentityCredential: *gm.identityCredential, + MembershipGroups: []keystore.MembershipGroup{{ + TreeIndex: *gm.membershipIndex, + MembershipContract: keystore.MembershipContract{ + ChainId: gm.chainId.String(), + Address: gm.membershipContractAddress.String(), + }, + }}, + } + + err := keystore.AddMembershipCredentials(path, []keystore.MembershipCredentials{keystoreCred}, password, RLNAppInfo, keystore.DefaultSeparator) + if err != nil { + return errors.New("failed to persist credentials") + } + + return nil +} + func (gm *DynamicGroupManager) InsertMember(pubkey rln.IDCommitment) error { gm.log.Debug("a new key is added", zap.Binary("pubkey", pubkey[:])) // assuming all the members arrive in order diff --git a/waku/v2/protocol/rln/group_manager/dynamic/web3.go b/waku/v2/protocol/rln/group_manager/dynamic/web3.go index 838f2f0c..a4cb26d4 100644 --- a/waku/v2/protocol/rln/group_manager/dynamic/web3.go +++ b/waku/v2/protocol/rln/group_manager/dynamic/web3.go @@ -8,7 +8,6 @@ import ( "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/event" @@ -26,18 +25,7 @@ func toBigInt(i []byte) *big.Int { return result } -func register(ctx context.Context, idComm r.IDCommitment, ethAccountPrivateKey *ecdsa.PrivateKey, ethClientAddress string, membershipContractAddress common.Address, registrationHandler RegistrationHandler, log *zap.Logger) (*r.MembershipIndex, error) { - backend, err := ethclient.Dial(ethClientAddress) - if err != nil { - return nil, err - } - defer backend.Close() - - chainID, err := backend.ChainID(ctx) - if err != nil { - return nil, err - } - +func register(ctx context.Context, backend *ethclient.Client, idComm r.IDCommitment, ethAccountPrivateKey *ecdsa.PrivateKey, rlnContract *contracts.RLN, chainID *big.Int, registrationHandler RegistrationHandler, log *zap.Logger) (*r.MembershipIndex, error) { auth, err := bind.NewKeyedTransactorWithChainID(ethAccountPrivateKey, chainID) if err != nil { return nil, err @@ -45,11 +33,6 @@ func register(ctx context.Context, idComm r.IDCommitment, ethAccountPrivateKey * auth.Value = MEMBERSHIP_FEE auth.Context = ctx - rlnContract, err := contracts.NewRLN(membershipContractAddress, backend) - if err != nil { - return nil, err - } - log.Debug("registering an id commitment", zap.Binary("idComm", idComm[:])) // registers the idComm into the membership contract whose address is in rlnPeer.membershipContractAddress @@ -100,8 +83,14 @@ func register(ctx context.Context, idComm r.IDCommitment, ethAccountPrivateKey * // Register registers the public key of the rlnPeer which is rlnPeer.membershipKeyPair.publicKey // into the membership contract whose address is in rlnPeer.membershipContractAddress func (gm *DynamicGroupManager) Register(ctx context.Context) (*r.MembershipIndex, error) { - pk := gm.identityCredential.IDCommitment - return register(ctx, pk, gm.ethAccountPrivateKey, gm.ethClientAddress, gm.membershipContractAddress, gm.registrationHandler, gm.log) + return register(ctx, + gm.ethClient, + gm.identityCredential.IDCommitment, + gm.ethAccountPrivateKey, + gm.rlnContract, + gm.chainId, + gm.registrationHandler, + gm.log) } // the types of inputs to this handler matches the MemberRegistered event/proc defined in the MembershipContract interface @@ -129,24 +118,13 @@ func (gm *DynamicGroupManager) processLogs(evt *contracts.RLNMemberRegistered, h func (gm *DynamicGroupManager) HandleGroupUpdates(ctx context.Context, handler RegistrationEventHandler) error { defer gm.wg.Done() - backend, err := ethclient.Dial(gm.ethClientAddress) - if err != nil { - return err - } - gm.ethClient = backend - - rlnContract, err := contracts.NewRLN(gm.membershipContractAddress, backend) - if err != nil { - return err - } - - err = gm.loadOldEvents(ctx, rlnContract, handler) + err := gm.loadOldEvents(ctx, gm.rlnContract, handler) if err != nil { return err } errCh := make(chan error) - go gm.watchNewEvents(ctx, rlnContract, handler, gm.log, errCh) + go gm.watchNewEvents(ctx, gm.rlnContract, handler, gm.log, errCh) return <-errCh } diff --git a/waku/v2/protocol/rln/keystore/keystore.go b/waku/v2/protocol/rln/keystore/keystore.go new file mode 100644 index 00000000..4418a4d6 --- /dev/null +++ b/waku/v2/protocol/rln/keystore/keystore.go @@ -0,0 +1,336 @@ +package keystore + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/waku-org/go-zerokit-rln/rln" + "go.uber.org/zap" +) + +const RLN_CREDENTIALS_FILENAME = "rlnCredentials.json" +const RLN_CREDENTIALS_PASSWORD = "password" + +type MembershipContract struct { + ChainId string `json:"chainId"` + Address string `json:"address"` +} + +type MembershipGroup struct { + MembershipContract MembershipContract `json:"membershipContract"` + TreeIndex rln.MembershipIndex `json:"treeIndex"` +} + +type MembershipCredentials struct { + IdentityCredential rln.IdentityCredential `json:"identityCredential"` + MembershipGroups []MembershipGroup `json:"membershipGroups"` +} + +type AppInfo struct { + Application string `json:"application"` + AppIdentifier string `json:"appIdentifier"` + Version string `json:"version"` +} + +type AppKeystore struct { + Application string `json:"application"` + AppIdentifier string `json:"appIdentifier"` + Credentials []keystore.CryptoJSON `json:"credentials"` + Version string `json:"version"` +} + +const DefaultSeparator = "\n" + +func (m MembershipCredentials) Equals(other MembershipCredentials) bool { + if !rln.IdentityCredentialEquals(m.IdentityCredential, other.IdentityCredential) { + return false + } + + for _, x := range m.MembershipGroups { + found := false + for _, y := range other.MembershipGroups { + if x.Equals(y) { + found = true + break + } + } + if !found { + return false + } + } + + return true +} + +func (m MembershipGroup) Equals(other MembershipGroup) bool { + return m.MembershipContract.Equals(other.MembershipContract) && m.TreeIndex == other.TreeIndex +} + +func (m MembershipContract) Equals(other MembershipContract) bool { + return m.Address == other.Address && m.ChainId == other.ChainId +} + +func CreateAppKeystore(path string, appInfo AppInfo, separator string) error { + if separator == "" { + separator = DefaultSeparator + } + + keystore := AppKeystore{ + AppIdentifier: appInfo.AppIdentifier, + Version: appInfo.Version, + } + + 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 ioutil.WriteFile(path, buffer.Bytes(), 0600) +} + +func LoadAppKeystore(path string, appInfo AppInfo, separator string) (AppKeystore, error) { + if separator == "" { + separator = DefaultSeparator + } + + _, 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, separator) + if err != nil { + return AppKeystore{}, err + } + } else { + return AppKeystore{}, err + } + } + + src, err := os.ReadFile(path) + if err != nil { + return AppKeystore{}, err + } + + for _, keystoreBytes := range bytes.Split(src, []byte(separator)) { + if len(keystoreBytes) == 0 { + continue + } + + keystore := AppKeystore{} + err := json.Unmarshal(keystoreBytes, &keystore) + if err != nil { + continue + } + + if keystore.AppIdentifier == appInfo.AppIdentifier && keystore.Application == appInfo.Application && keystore.Version == appInfo.Version { + return keystore, nil + } + } + + return AppKeystore{}, errors.New("no keystore found") +} + +func filterCredential(credential MembershipCredentials, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) *MembershipCredentials { + if len(filterIdentityCredentials) != 0 { + found := false + for _, filterCreds := range filterIdentityCredentials { + if filterCreds.Equals(credential) { + found = true + } + } + if !found { + return nil + } + } + + if len(filterMembershipContracts) != 0 { + var membershipGroupsIntersection []MembershipGroup + for _, filterContract := range filterMembershipContracts { + for _, credentialGroups := range credential.MembershipGroups { + if filterContract.Equals(credentialGroups.MembershipContract) { + membershipGroupsIntersection = append(membershipGroupsIntersection, credentialGroups) + } + } + } + if len(membershipGroupsIntersection) != 0 { + // If we have a match on some groups, we return the credential with filtered groups + return &MembershipCredentials{ + IdentityCredential: credential.IdentityCredential, + MembershipGroups: membershipGroupsIntersection, + } + } else { + return nil + } + } + + // We hit this return only if + // - filterIdentityCredentials.len() == 0 and filterMembershipContracts.len() == 0 (no filter) + // - filterIdentityCredentials.len() != 0 and filterMembershipContracts.len() == 0 (filter only on identity credential) + // Indeed, filterMembershipContracts.len() != 0 will have its exclusive return based on all values of membershipGroupsIntersection.len() + return &credential +} + +func GetMembershipCredentials(logger *zap.Logger, credentialsPath string, password string, appInfo AppInfo, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) ([]MembershipCredentials, error) { + k, err := LoadAppKeystore(credentialsPath, appInfo, DefaultSeparator) + if err != nil { + return nil, err + } + + var result []MembershipCredentials + + for _, credential := range k.Credentials { + credentialsBytes, err := keystore.DecryptDataV3(credential, password) + if err != nil { + return nil, err + } + + var credentials MembershipCredentials + err = json.Unmarshal(credentialsBytes, &credentials) + if err != nil { + return nil, err + } + + filteredCredential := filterCredential(credentials, filterIdentityCredentials, filterMembershipContracts) + if filterIdentityCredentials != nil { + result = append(result, *filteredCredential) + } + } + + return result, nil +} + +// Adds a sequence of membership credential to the keystore matching the application, appIdentifier and version filters. +func AddMembershipCredentials(path string, credentials []MembershipCredentials, password string, appInfo AppInfo, separator string) error { + k, err := LoadAppKeystore(path, appInfo, DefaultSeparator) + if err != nil { + return err + } + + var credentialsToAdd []MembershipCredentials + for _, newCredential := range credentials { + // A flag to tell us if the keystore contains a credential associated to the input identity credential, i.e. membershipCredential + found := -1 + for i, existingCredentials := range k.Credentials { + credentialsBytes, err := keystore.DecryptDataV3(existingCredentials, password) + if err != nil { + continue + } + + var credentials MembershipCredentials + err = json.Unmarshal(credentialsBytes, &credentials) + if err != nil { + continue + } + + if rln.IdentityCredentialEquals(credentials.IdentityCredential, newCredential.IdentityCredential) { + // idCredential is present in keystore. We add the input credential membership group to the one contained in the decrypted keystore credential (we deduplicate groups using sets) + allMemberships := append(credentials.MembershipGroups, newCredential.MembershipGroups...) + + // we define the updated credential with the updated membership sets + updatedCredential := MembershipCredentials{ + IdentityCredential: newCredential.IdentityCredential, + MembershipGroups: allMemberships, + } + + // we re-encrypt creating a new keyfile + b, err := json.Marshal(updatedCredential) + if err != nil { + return err + } + + encryptedCredentials, err := keystore.EncryptDataV3(b, []byte(password), keystore.StandardScryptN, keystore.StandardScryptP) + if err != nil { + return err + } + + // we update the original credential field in keystoreCredentials + k.Credentials[i] = encryptedCredentials + + found = i + + // We stop decrypting other credentials in the keystore + break + } + } + + if found == -1 { + credentialsToAdd = append(credentialsToAdd, newCredential) + } + } + + for _, c := range credentialsToAdd { + b, err := json.Marshal(c) + if err != nil { + return err + } + + encryptedCredentials, err := keystore.EncryptDataV3(b, []byte(password), keystore.StandardScryptN, keystore.StandardScryptP) + if err != nil { + return err + } + + k.Credentials = append(k.Credentials, encryptedCredentials) + } + + return save(k, path, separator) +} + +// 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, separator 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 + } + } + + if separator == "" { + separator = DefaultSeparator + } + + 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 { + restoreErr := os.Rename(path, path+".bkp") + if restoreErr != nil { + return fmt.Errorf("could not restore backup file: %w", restoreErr) + } + return err + } + + err = ioutil.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 + } + + return nil +} diff --git a/waku/v2/protocol/rln/waku_rln_relay.go b/waku/v2/protocol/rln/waku_rln_relay.go index 88a286f7..063f60ba 100644 --- a/waku/v2/protocol/rln/waku_rln_relay.go +++ b/waku/v2/protocol/rln/waku_rln_relay.go @@ -21,12 +21,6 @@ import ( proto "google.golang.org/protobuf/proto" ) -var RLNAppInfo = AppInfo{ - Application: "go-waku-rln-relay", - AppIdentifier: "01234567890abcdef", - Version: "0.1", -} - type GroupManager interface { Start(ctx context.Context, rln *rln.RLN, rootTracker *group_manager.MerkleRootTracker) error IdentityCredentials() (rln.IdentityCredential, error)