From eeb8e908fde4f01d67cbe5e87ac65b8278b6f41c Mon Sep 17 00:00:00 2001 From: Andrej Zavgorodnij Date: Thu, 24 Sep 2020 20:12:38 +0300 Subject: [PATCH 1/2] feat: airgapped state --- airgapped/airgapped.go | 119 ++++++++++++++++- airgapped/airgapped_test.go | 253 +++++++++++++++++++++++++++++++++++- airgapped/dkg.go | 5 +- airgapped/types.go | 7 +- dkg/dkg.go | 31 ++++- dkg/types.go | 10 +- go.mod | 6 + go.sum | 9 +- 8 files changed, 419 insertions(+), 21 deletions(-) diff --git a/airgapped/airgapped.go b/airgapped/airgapped.go index 6ae80ac..a54fb43 100644 --- a/airgapped/airgapped.go +++ b/airgapped/airgapped.go @@ -8,6 +8,8 @@ import ( "os" "sync" + bls12381 "github.com/depools/kyber-bls12381" + vss "github.com/corestario/kyber/share/vss/rabin" "github.com/corestario/kyber" @@ -20,7 +22,6 @@ import ( "github.com/depools/dc4bc/fsm/state_machines/signing_proposal_fsm" "github.com/depools/dc4bc/fsm/types/requests" "github.com/depools/dc4bc/qr" - bls12381 "github.com/depools/kyber-bls12381" "github.com/syndtr/goleveldb/leveldb" ) @@ -28,6 +29,7 @@ const ( resultQRFolder = "result_qr_codes" pubKeyDBKey = "public_key" privateKeyDBKey = "private_key" + operationsLogDBKey = "operations_log" participantAddressKey = "participant_address" saltKey = "salt_key" ) @@ -44,6 +46,7 @@ type AirgappedMachine struct { pubKey kyber.Point secKey kyber.Scalar suite vss.Suite + seed []byte db *leveldb.DB } @@ -64,19 +67,120 @@ func NewAirgappedMachine(dbPath string) (*AirgappedMachine, error) { qrProcessor: qr.NewCameraProcessor(), } - am.suite = bls12381.NewBLS12381Suite() - if am.db, err = leveldb.OpenFile(dbPath, nil); err != nil { return nil, fmt.Errorf("failed to open db file %s for keys: %w", dbPath, err) } + seed, err := am.getSeed() + if err != nil { + seed = make([]byte, 32) + _, _ = rand.Read(seed) + if err := am.storeSeed(seed); err != nil { + panic(err) + } + } + am.seed = seed + am.suite = bls12381.NewBLS12381Suite(am.seed) + if err = am.loadAddressFromDB(dbPath); err != nil { return nil, fmt.Errorf("failed to load address from db") } + if _, err = am.db.Get([]byte(operationsLogDBKey), nil); err != nil { + if err == leveldb.ErrNotFound { + operationsLogBz, _ := json.Marshal([]client.Operation{}) + if err := am.db.Put([]byte(operationsLogDBKey), operationsLogBz, nil); err != nil { + return nil, fmt.Errorf("failed to init Operation log: %w", err) + } + } else { + return nil, fmt.Errorf("failed to init Operation log (fatal): %w", err) + } + } + return am, nil } +func (am *AirgappedMachine) storeSeed(seed []byte) error { + if err := am.db.Put([]byte("seedKey"), seed, nil); err != nil { + return fmt.Errorf("failed to put seed: %w", err) + } + + return nil +} + +func (am *AirgappedMachine) getSeed() ([]byte, error) { + seed, err := am.db.Get([]byte("seedKey"), nil) + if err != nil { + return nil, fmt.Errorf("failed to get seed: %w", err) + } + + return seed, nil +} + +func (am *AirgappedMachine) storeOperation(o client.Operation) error { + operationsLogBz, err := am.db.Get([]byte(operationsLogDBKey), nil) + if err != nil { + if err == leveldb.ErrNotFound { + return err + } + return fmt.Errorf("failed to get operationsLogBz from db: %w", err) + } + + var operationsLog []client.Operation + if err := json.Unmarshal(operationsLogBz, &operationsLog); err != nil { + return fmt.Errorf("failed to unmarshal stored operationsLog: %w", err) + } + + operationsLog = append(operationsLog, o) + + operationsLogBz, err = json.Marshal(operationsLog) + if err != nil { + return fmt.Errorf("failed to marshal operationsLog: %w", err) + } + + if err := am.db.Put([]byte(operationsLogDBKey), operationsLogBz, nil); err != nil { + return fmt.Errorf("failed to put updated operationsLog: %w", err) + } + + return nil +} + +func (am *AirgappedMachine) getOperationsLog() ([]client.Operation, error) { + operationsLogBz, err := am.db.Get([]byte(operationsLogDBKey), nil) + if err != nil { + if err == leveldb.ErrNotFound { + return nil, err + } + return nil, fmt.Errorf("failed to get public key from db: %w", err) + } + + var operationsLog []client.Operation + if err := json.Unmarshal(operationsLogBz, &operationsLog); err != nil { + return nil, fmt.Errorf("failed to unmarshal stored operationsLog: %w", err) + } + + return operationsLog, nil +} + +func (am *AirgappedMachine) ReplayOperationsLog() error { + operationsLog, err := am.getOperationsLog() + if err != nil { + return fmt.Errorf("failed to getOperationsLog: %w", err) + } + + for _, operation := range operationsLog { + if _, err := am.HandleOperation(operation); err != nil { + return fmt.Errorf( + "failed to HandleOperation %s (this error is fatal, the state can not be recovered): %w", + operation.ID, err) + } + } + + log.Println("Successfully replayed Operation log") + + return nil +} + func (am *AirgappedMachine) InitKeys() error { err := am.LoadKeysFromDB() if err != nil && err != leveldb.ErrNotFound { @@ -147,6 +251,7 @@ func (am *AirgappedMachine) LoadKeysFromDB() error { if err != nil { return err } + decryptedPrivateKey, err := decrypt(am.encryptionKey, salt, privateKeyBz) if err != nil { return err @@ -263,6 +368,14 @@ func (am *AirgappedMachine) decryptDataFromParticipant(data []byte) ([]byte, err } func (am *AirgappedMachine) HandleOperation(operation client.Operation) (client.Operation, error) { + if err := am.storeOperation(operation); err != nil { + return client.Operation{}, fmt.Errorf("failed to storeOperation: %w", err) + } + + return am.handleOperation(operation) +} + +func (am *AirgappedMachine) handleOperation(operation client.Operation) (client.Operation, error) { var ( err error ) diff --git a/airgapped/airgapped_test.go b/airgapped/airgapped_test.go index 6a5f6c6..7799a68 100644 --- a/airgapped/airgapped_test.go +++ b/airgapped/airgapped_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + client "github.com/depools/dc4bc/client/types" "github.com/depools/dc4bc/fsm/fsm" "github.com/depools/dc4bc/fsm/state_machines/dkg_proposal_fsm" @@ -311,12 +313,261 @@ func TestAirgappedAllSteps(t *testing.T) { fmt.Println("DKG succeeded, signature recovered") } +func TestAirgappedMachine_Replay(t *testing.T) { + testDir := "/tmp/airgapped_test" + nodesCount := 2 + threshold := 2 + participants := make([]string, nodesCount) + for i := 0; i < nodesCount; i++ { + participants[i] = fmt.Sprintf("Participant#%d", i) + } + + tr := &Transport{} + for i := 0; i < nodesCount; i++ { + am, err := NewAirgappedMachine(fmt.Sprintf("%s/%s-%d", testDir, testDB, i)) + if err != nil { + t.Fatalf("failed to create airgapped machine: %v", err) + } + am.SetAddress(participants[i]) + am.SetEncryptionKey([]byte(fmt.Sprintf(testDB+"%d", i))) + if err = am.InitKeys(); err != nil { + t.Fatalf(err.Error()) + } + node := Node{ + ParticipantID: i, + Participant: participants[i], + Machine: am, + } + tr.nodes = append(tr.nodes, &node) + } + defer os.RemoveAll(testDir) + + var initReq responses.SignatureProposalParticipantInvitationsResponse + for _, n := range tr.nodes { + pubKey, err := n.Machine.pubKey.MarshalBinary() + if err != nil { + t.Fatalf("failed to marshal dkg pubkey: %v", err) + } + entry := &responses.SignatureProposalParticipantInvitationEntry{ + ParticipantId: n.ParticipantID, + Addr: n.Participant, + Threshold: threshold, + DkgPubKey: pubKey, + } + initReq = append(initReq, entry) + } + op := createOperation(t, string(signature_proposal_fsm.StateAwaitParticipantsConfirmations), "", initReq) + runStep(tr, func(n *Node, wg *sync.WaitGroup) { + defer wg.Done() + + _, err := n.Machine.HandleOperation(op) + if err != nil { + t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err) + } + }) + + // get commits + var getCommitsRequest responses.DKGProposalPubKeysParticipantResponse + for _, n := range tr.nodes { + pubKey, err := n.Machine.pubKey.MarshalBinary() + if err != nil { + t.Fatalf("%s: failed to marshal pubkey: %v", n.Participant, err) + } + entry := &responses.DKGProposalPubKeysParticipantEntry{ + ParticipantId: n.ParticipantID, + Addr: n.Participant, + DkgPubKey: pubKey, + } + getCommitsRequest = append(getCommitsRequest, entry) + } + op = createOperation(t, string(dkg_proposal_fsm.StateDkgCommitsAwaitConfirmations), "", getCommitsRequest) + runStep(tr, func(n *Node, wg *sync.WaitGroup) { + defer wg.Done() + + operation, err := n.Machine.HandleOperation(op) + if err != nil { + t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err) + } + for _, msg := range operation.ResultMsgs { + tr.BroadcastMessage(t, msg) + } + }) + + //deals + runStep(tr, func(n *Node, wg *sync.WaitGroup) { + defer wg.Done() + + var payload responses.DKGProposalCommitParticipantResponse + for _, req := range n.commits { + p := responses.DKGProposalCommitParticipantEntry{ + ParticipantId: req.ParticipantId, + Addr: fmt.Sprintf("Participant#%d", req.ParticipantId), + DkgCommit: req.Commit, + } + payload = append(payload, &p) + } + op := createOperation(t, string(dkg_proposal_fsm.StateDkgDealsAwaitConfirmations), "", payload) + + operation, err := n.Machine.HandleOperation(op) + if err != nil { + t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err) + } + for _, msg := range operation.ResultMsgs { + tr.BroadcastMessage(t, msg) + } + }) + + // At this point something goes wrong and we have to restart the machines. + for _, node := range tr.nodes { + _ = node.Machine.db.Close() + } + + participants = make([]string, nodesCount) + for i := 0; i < nodesCount; i++ { + participants[i] = fmt.Sprintf("Participant#%d", i) + } + + newTr := &Transport{} + for i := 0; i < nodesCount; i++ { + am, err := NewAirgappedMachine(fmt.Sprintf("%s/%s-%d", testDir, testDB, i)) + if err != nil { + t.Fatalf("failed to create airgapped machine: %v", err) + } + _ = am.SetAddress(participants[i]) + am.SetEncryptionKey([]byte(fmt.Sprintf(testDB+"%d", i))) + if err = am.InitKeys(); err != nil { + t.Fatalf(err.Error()) + } + node := Node{ + ParticipantID: i, + Participant: participants[i], + Machine: am, + deals: tr.nodes[i].deals, + } + newTr.nodes = append(newTr.nodes, &node) + } + defer os.RemoveAll(testDir) + + for _, node := range newTr.nodes { + err := node.Machine.ReplayOperationsLog() + require.NoError(t, err) + } + + //oldTr := tr + tr = newTr + + //responses + runStep(tr, func(n *Node, wg *sync.WaitGroup) { + defer wg.Done() + + var payload responses.DKGProposalDealParticipantResponse + for _, req := range n.deals { + p := responses.DKGProposalDealParticipantEntry{ + ParticipantId: req.ParticipantId, + Addr: fmt.Sprintf("Participant#%d", req.ParticipantId), + DkgDeal: req.Deal, + } + payload = append(payload, &p) + } + op := createOperation(t, string(dkg_proposal_fsm.StateDkgResponsesAwaitConfirmations), "", payload) + + operation, err := n.Machine.HandleOperation(op) + if err != nil { + t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err) + } + for _, msg := range operation.ResultMsgs { + tr.BroadcastMessage(t, msg) + } + }) + + //master key + runStep(tr, func(n *Node, wg *sync.WaitGroup) { + defer wg.Done() + + var payload responses.DKGProposalResponseParticipantResponse + for _, req := range n.responses { + p := responses.DKGProposalResponseParticipantEntry{ + ParticipantId: req.ParticipantId, + Addr: fmt.Sprintf("Participant#%d", req.ParticipantId), + DkgResponse: req.Response, + } + payload = append(payload, &p) + } + op := createOperation(t, string(dkg_proposal_fsm.StateDkgMasterKeyAwaitConfirmations), "", payload) + + operation, err := n.Machine.HandleOperation(op) + if err != nil { + t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err) + } + for _, msg := range operation.ResultMsgs { + tr.BroadcastMessage(t, msg) + } + }) + + // check that all master keys are equal + for _, n := range tr.nodes { + for i := 0; i < len(n.masterKeys); i++ { + if !bytes.Equal(n.masterKeys[0].MasterKey, n.masterKeys[i].MasterKey) { + t.Fatalf("master keys is not equal!") + } + } + } + + msgToSign := []byte("i am a message") + + //partialSigns + runStep(tr, func(n *Node, wg *sync.WaitGroup) { + defer wg.Done() + + payload := responses.SigningPartialSignsParticipantInvitationsResponse{ + SrcPayload: msgToSign, + } + + op := createOperation(t, string(signing_proposal_fsm.StateSigningAwaitPartialSigns), "", payload) + + operation, err := n.Machine.HandleOperation(op) + if err != nil { + t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err) + } + for _, msg := range operation.ResultMsgs { + tr.BroadcastMessage(t, msg) + } + }) + + //recover full signature + runStep(tr, func(n *Node, wg *sync.WaitGroup) { + defer wg.Done() + + var payload responses.SigningProcessParticipantResponse + for _, req := range n.partialSigns { + p := responses.SigningProcessParticipantEntry{ + ParticipantId: req.ParticipantId, + Addr: fmt.Sprintf("Participant#%d", req.ParticipantId), + PartialSign: req.PartialSign, + } + payload.Participants = append(payload.Participants, &p) + } + payload.SrcPayload = msgToSign + op := createOperation(t, string(signing_proposal_fsm.StateSigningPartialSignsCollected), "", payload) + + operation, err := n.Machine.HandleOperation(op) + if err != nil { + t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err) + } + for _, msg := range operation.ResultMsgs { + tr.BroadcastMessage(t, msg) + } + }) + + fmt.Println("DKG succeeded, signature recovered") +} + func runStep(transport *Transport, cb func(n *Node, wg *sync.WaitGroup)) { var wg = &sync.WaitGroup{} for _, node := range transport.nodes { wg.Add(1) n := node - go cb(n, wg) + cb(n, wg) } wg.Wait() } diff --git a/airgapped/dkg.go b/airgapped/dkg.go index 00bcf3e..c0b41fd 100644 --- a/airgapped/dkg.go +++ b/airgapped/dkg.go @@ -13,7 +13,6 @@ import ( "github.com/depools/dc4bc/fsm/types/requests" "github.com/depools/dc4bc/fsm/types/responses" "github.com/depools/dc4bc/storage" - bls12381 "github.com/depools/kyber-bls12381" ) func createMessage(o client.Operation, data []byte) storage.Message { @@ -97,14 +96,14 @@ func (am *AirgappedMachine) handleStateDkgCommitsAwaitConfirmations(o *client.Op } for _, entry := range payload { - pubKey := bls12381.NewBLS12381Suite().Point() + pubKey := am.suite.Point() if err = pubKey.UnmarshalBinary(entry.DkgPubKey); err != nil { return fmt.Errorf("failed to unmarshal pubkey: %w", err) } dkgInstance.StorePubKey(entry.Addr, entry.ParticipantId, pubKey) } - if err = dkgInstance.InitDKGInstance(); err != nil { + if err = dkgInstance.InitDKGInstance(am.seed); err != nil { return fmt.Errorf("failed to init dkg instance: %w", err) } diff --git a/airgapped/types.go b/airgapped/types.go index 78ab194..638e3a3 100644 --- a/airgapped/types.go +++ b/airgapped/types.go @@ -2,9 +2,10 @@ package airgapped import ( "fmt" + "strings" + "github.com/depools/dc4bc/dkg" "github.com/syndtr/goleveldb/leveldb/util" - "strings" ) const ( @@ -57,7 +58,7 @@ func (am *AirgappedMachine) loadBLSKeyring(dkgID string) (*dkg.BLSKeyring, error return nil, fmt.Errorf("failed to decrypt BLS keyring: %w", err) } - if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(decryptedKeyring); err != nil { + if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.suite, decryptedKeyring); err != nil { return nil, fmt.Errorf("failed to decode bls keyring") } return blsKeyring, nil @@ -85,7 +86,7 @@ func (am *AirgappedMachine) GetBLSKeyrings() (map[string]*dkg.BLSKeyring, error) if err != nil { return nil, fmt.Errorf("failed to decrypt BLS keyring: %w", err) } - if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(decryptedKeyring); err != nil { + if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.suite, decryptedKeyring); err != nil { return nil, fmt.Errorf("failed to decode bls keyring: %w", err) } keyrings[strings.TrimLeft(string(key), blsKeyringPrefix)] = blsKeyring diff --git a/dkg/dkg.go b/dkg/dkg.go index 3935566..857e102 100644 --- a/dkg/dkg.go +++ b/dkg/dkg.go @@ -7,11 +7,12 @@ import ( "sort" "sync" - "github.com/corestario/kyber/share" - "github.com/corestario/kyber" + "github.com/corestario/kyber/share" dkg "github.com/corestario/kyber/share/dkg/pedersen" vss "github.com/corestario/kyber/share/vss/pedersen" + "github.com/google/go-cmp/cmp" + "lukechampine.com/frand" ) // TODO: dump necessary data on disk @@ -46,6 +47,26 @@ func Init(suite vss.Suite, pubKey kyber.Point, secKey kyber.Scalar) *DKG { return &d } +func (d *DKG) Equals(other *DKG) error { + for addr, commits := range d.commits { + otherCommits := other.commits[addr] + for idx := range commits { + if !commits[idx].Equal(otherCommits[idx]) { + return fmt.Errorf("commits from %s are not equal (idx %d): %v != %v", addr, idx, commits[idx], otherCommits[idx]) + } + } + } + + for addr, deal := range d.deals { + otherDeal := other.deals[addr] + if !cmp.Equal(deal.Deal, otherDeal.Deal) { + return fmt.Errorf("deals from %s are not equal: %+v != %+v", addr, deal.Deal, otherDeal.Deal) + } + } + + return nil +} + func (d *DKG) GetPubKey() kyber.Point { return d.pubKey } @@ -90,7 +111,7 @@ func (d *DKG) calcParticipantID() int { return -1 } -func (d *DKG) InitDKGInstance() (err error) { +func (d *DKG) InitDKGInstance(seed []byte) (err error) { sort.Sort(d.pubkeys) publicKeys := d.pubkeys.GetPKs() @@ -107,7 +128,9 @@ func (d *DKG) InitDKGInstance() (err error) { d.responses = newMessageStore(int(math.Pow(float64(participantsCount)-1, 2))) - d.instance, err = dkg.NewDistKeyGenerator(d.suite, d.secKey, publicKeys, d.Threshold) + reader := frand.NewCustom(seed, 32, 20) + + d.instance, err = dkg.NewDistKeyGenerator(d.suite, d.secKey, publicKeys, d.Threshold, reader) if err != nil { return err } diff --git a/dkg/types.go b/dkg/types.go index 9bdf08d..8dfd674 100644 --- a/dkg/types.go +++ b/dkg/types.go @@ -7,10 +7,10 @@ import ( "fmt" "github.com/corestario/kyber/pairing" + vss "github.com/corestario/kyber/share/vss/pedersen" "github.com/corestario/kyber" "github.com/corestario/kyber/share" - bls12381 "github.com/depools/kyber-bls12381" ) type PK2Participant struct { @@ -143,7 +143,7 @@ func (b *BLSKeyring) Bytes() ([]byte, error) { return json.Marshal(blsKeyringJSON) } -func LoadBLSKeyringFromBytes(data []byte) (*BLSKeyring, error) { +func LoadBLSKeyringFromBytes(suite vss.Suite, data []byte) (*BLSKeyring, error) { var ( err error blsKeyringJson blsKeyringJSON @@ -154,20 +154,20 @@ func LoadBLSKeyringFromBytes(data []byte) (*BLSKeyring, error) { commitments := make([]kyber.Point, 0, len(blsKeyringJson.Commitments)) for _, commitmentBz := range blsKeyringJson.Commitments { - commitment := bls12381.NewBLS12381Suite().Point() + commitment := suite.Point() if err := commitment.UnmarshalBinary(commitmentBz); err != nil { return nil, fmt.Errorf("failed to unmarshal commitment: %w", err) } commitments = append(commitments, commitment) } - priShare, privDec := &share.PriShare{V: bls12381.NewBLS12381Suite().(pairing.Suite).G1().Scalar()}, gob.NewDecoder(bytes.NewBuffer(blsKeyringJson.Share)) + priShare, privDec := &share.PriShare{V: suite.(pairing.Suite).G1().Scalar()}, gob.NewDecoder(bytes.NewBuffer(blsKeyringJson.Share)) if err := privDec.Decode(priShare); err != nil { return nil, fmt.Errorf("failed to share: %v", err) } return &BLSKeyring{ - PubPoly: share.NewPubPoly(bls12381.NewBLS12381Suite(), nil, commitments), + PubPoly: share.NewPubPoly(suite, nil, commitments), Share: priShare, }, nil } diff --git a/go.mod b/go.mod index 65d1834..a74d208 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/corestario/kyber v1.3.0 github.com/depools/kyber-bls12381 v0.0.0-20200831104422-978ac58f592e github.com/golang/mock v1.4.4 + github.com/google/go-cmp v0.2.0 github.com/google/uuid v1.1.1 github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b github.com/looplab/fsm v0.1.0 @@ -19,6 +20,11 @@ require ( golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a golang.org/x/text v0.3.3 // indirect golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect + lukechampine.com/frand v1.3.0 ) replace golang.org/x/crypto => github.com/tendermint/crypto v0.0.0-20180820045704-3764759f34a5 + +replace github.com/corestario/kyber => ../kyber + +replace github.com/depools/kyber-bls12381 => ../kyber-bls12381 diff --git a/go.sum b/go.sum index 97cf72b..9f14267 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -13,8 +15,6 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/corestario/kyber v1.3.0 h1:SEWofdorUUeAJTsa9WJmrUYFyWHSWyXLgqDTFFEIzes= -github.com/corestario/kyber v1.3.0/go.mod h1:kIWfWekm8kSJNti3Fo3DCV0GHEH050MWQrdvZdefbkk= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -47,7 +47,9 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -180,6 +182,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -216,3 +219,5 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/frand v1.3.0 h1:HFLrwEHr78+EqAfyp8OChgEzdYCVZzzj6Y+cGDQRhaI= +lukechampine.com/frand v1.3.0/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= From 433dc14aaf9130e5613a46844ef07d9293dca600 Mon Sep 17 00:00:00 2001 From: Andrej Zavgorodnij Date: Tue, 29 Sep 2020 13:10:16 +0300 Subject: [PATCH 2/2] Added airgapped commands for log replay --- Makefile | 12 +- airgapped/airgapped.go | 216 ++++---------------------------- airgapped/airgapped_test.go | 4 +- airgapped/bls.go | 6 +- airgapped/dkg.go | 21 +++- airgapped/storage.go | 242 ++++++++++++++++++++++++++++++++++++ airgapped/types.go | 10 +- client/flow_test.go | 5 +- cmd/airgapped/main.go | 52 +++++++- 9 files changed, 351 insertions(+), 217 deletions(-) create mode 100644 airgapped/storage.go diff --git a/Makefile b/Makefile index aeaa946..b0eeac8 100644 --- a/Makefile +++ b/Makefile @@ -12,18 +12,18 @@ mocks: build-darwin: @echo "Building dc4bc_d..." - GOOS=darwin GOARCH=amd64 go build -o dc4bc_d_darwin ./cmd/dc4bc_d/main.go + GOOS=darwin GOARCH=amd64 go build -o dc4bc_d_darwin ./cmd/dc4bc_d/ @echo "Building dc4bc_cli..." - GOOS=darwin GOARCH=amd64 go build -o dc4bc_cli_darwin ./cmd/dc4bc_cli/main.go + GOOS=darwin GOARCH=amd64 go build -o dc4bc_cli_darwin ./cmd/dc4bc_cli/ @echo "Building dc4bc_airgapped..." - GOOS=darwin GOARCH=amd64 go build -o dc4bc_airgapped_darwin ./cmd/airgapped/main.go + GOOS=darwin GOARCH=amd64 go build -o dc4bc_airgapped_darwin ./cmd/airgapped/ build-linux: @echo "Building dc4bc_d..." - GOOS=linux GOARCH=amd64 go build -o dc4bc_d_linux ./cmd/dc4bc_d/main.go + GOOS=linux GOARCH=amd64 go build -o dc4bc_d_linux ./cmd/dc4bc_d/ @echo "Building dc4bc_cli..." - GOOS=linux GOARCH=amd64 go build -o dc4bc_cli_linux ./cmd/dc4bc_cli/main.go + GOOS=linux GOARCH=amd64 go build -o dc4bc_cli_linux ./cmd/dc4bc_cli/ @echo "Building dc4bc_airgapped..." - GOOS=linux GOARCH=amd64 go build -o dc4bc_airgapped_linux ./cmd/airgapped/main.go + GOOS=linux GOARCH=amd64 go build -o dc4bc_airgapped_linux ./cmd/airgapped/ .PHONY: mocks diff --git a/airgapped/airgapped.go b/airgapped/airgapped.go index eb79b28..a1bb550 100644 --- a/airgapped/airgapped.go +++ b/airgapped/airgapped.go @@ -1,15 +1,12 @@ package airgapped import ( - "crypto/rand" "encoding/json" "fmt" "log" "os" "sync" - bls12381 "github.com/depools/kyber-bls12381" - vss "github.com/corestario/kyber/share/vss/rabin" "github.com/corestario/kyber" @@ -26,11 +23,8 @@ import ( ) const ( - resultQRFolder = "result_qr_codes" - pubKeyDBKey = "public_key" - privateKeyDBKey = "private_key" - saltKey = "salt_key" - operationsLogDBKey = "operations_log" + resultQRFolder = "result_qr_codes" + seedSize = 32 ) type AirgappedMachine struct { @@ -42,8 +36,8 @@ type AirgappedMachine struct { encryptionKey []byte pubKey kyber.Point secKey kyber.Scalar - suite vss.Suite - seed []byte + baseSuite vss.Suite + baseSeed []byte db *leveldb.DB } @@ -68,20 +62,13 @@ func NewAirgappedMachine(dbPath string) (*AirgappedMachine, error) { return nil, fmt.Errorf("failed to open db file %s for keys: %w", dbPath, err) } - seed, err := am.getSeed() - if err != nil { - seed = make([]byte, 32) - _, _ = rand.Read(seed) - if err := am.storeSeed(seed); err != nil { - panic(err) - } + if err := am.loadBaseSeed(); err != nil { + return nil, fmt.Errorf("failed to loadBaseSeed: %w", err) } - am.seed = seed - am.suite = bls12381.NewBLS12381Suite(am.seed) if _, err = am.db.Get([]byte(operationsLogDBKey), nil); err != nil { if err == leveldb.ErrNotFound { - operationsLogBz, _ := json.Marshal([]client.Operation{}) + operationsLogBz, _ := json.Marshal(RoundOperationLog{}) if err := am.db.Put([]byte(operationsLogDBKey), operationsLogBz, nil); err != nil { return nil, fmt.Errorf("failed to init Operation log: %w", err) } @@ -93,87 +80,6 @@ func NewAirgappedMachine(dbPath string) (*AirgappedMachine, error) { return am, nil } -func (am *AirgappedMachine) storeSeed(seed []byte) error { - if err := am.db.Put([]byte("seedKey"), seed, nil); err != nil { - return fmt.Errorf("failed to put seed: %w", err) - } - - return nil -} - -func (am *AirgappedMachine) getSeed() ([]byte, error) { - seed, err := am.db.Get([]byte("seedKey"), nil) - if err != nil { - return nil, fmt.Errorf("failed to get seed: %w", err) - } - - return seed, nil -} - -func (am *AirgappedMachine) storeOperation(o client.Operation) error { - operationsLogBz, err := am.db.Get([]byte(operationsLogDBKey), nil) - if err != nil { - if err == leveldb.ErrNotFound { - return err - } - return fmt.Errorf("failed to get operationsLogBz from db: %w", err) - } - - var operationsLog []client.Operation - if err := json.Unmarshal(operationsLogBz, &operationsLog); err != nil { - return fmt.Errorf("failed to unmarshal stored operationsLog: %w", err) - } - - operationsLog = append(operationsLog, o) - - operationsLogBz, err = json.Marshal(operationsLog) - if err != nil { - return fmt.Errorf("failed to marshal operationsLog: %w", err) - } - - if err := am.db.Put([]byte(operationsLogDBKey), operationsLogBz, nil); err != nil { - return fmt.Errorf("failed to put updated operationsLog: %w", err) - } - - return nil -} - -func (am *AirgappedMachine) getOperationsLog() ([]client.Operation, error) { - operationsLogBz, err := am.db.Get([]byte(operationsLogDBKey), nil) - if err != nil { - if err == leveldb.ErrNotFound { - return nil, err - } - return nil, fmt.Errorf("failed to get public key from db: %w", err) - } - - var operationsLog []client.Operation - if err := json.Unmarshal(operationsLogBz, &operationsLog); err != nil { - return nil, fmt.Errorf("failed to unmarshal stored operationsLog: %w", err) - } - - return operationsLog, nil -} - -func (am *AirgappedMachine) ReplayOperationsLog() error { - operationsLog, err := am.getOperationsLog() - if err != nil { - return fmt.Errorf("failed to getOperationsLog: %w", err) - } - - for _, operation := range operationsLog { - if _, err := am.HandleOperation(operation); err != nil { - return fmt.Errorf( - "failed to HandleOperation %s (this error is fatal, the state can not be recovered): %w", - operation.ID, err) - } - } - - log.Println("Successfully replayed Operation log") - - return nil -} - // InitKeys load keys public and private keys for DKG from LevelDB. If keys does not exist, creates them. func (am *AirgappedMachine) InitKeys() error { err := am.LoadKeysFromDB() @@ -182,8 +88,8 @@ func (am *AirgappedMachine) InitKeys() error { } // if keys were not generated yet if err == leveldb.ErrNotFound { - am.secKey = am.suite.Scalar().Pick(am.suite.RandomStream()) - am.pubKey = am.suite.Point().Mul(am.secKey, nil) + am.secKey = am.baseSuite.Scalar().Pick(am.baseSuite.RandomStream()) + am.pubKey = am.baseSuite.Point().Mul(am.secKey, nil) return am.SaveKeysToDB() } @@ -211,101 +117,27 @@ func (am *AirgappedMachine) DropSensitiveData() { am.encryptionKey = nil } -// LoadKeysFromDB load DKG keys from LevelDB -func (am *AirgappedMachine) LoadKeysFromDB() error { - pubKeyBz, err := am.db.Get([]byte(pubKeyDBKey), nil) +func (am *AirgappedMachine) ReplayOperationsLog(dkgIdentifier string) error { + operationsLog, err := am.getOperationsLog(dkgIdentifier) if err != nil { - if err == leveldb.ErrNotFound { - return err + return fmt.Errorf("failed to getOperationsLog: %w", err) + } + + for _, operation := range operationsLog { + if _, err := am.HandleOperation(operation); err != nil { + return fmt.Errorf( + "failed to HandleOperation %s (this error is fatal, the state can not be recovered): %w", + operation.ID, err) } - return fmt.Errorf("failed to get public key from db: %w", err) } - privateKeyBz, err := am.db.Get([]byte(privateKeyDBKey), nil) - if err != nil { - if err == leveldb.ErrNotFound { - return err - } - return fmt.Errorf("failed to get private key from db: %w", err) - } + log.Println("Successfully replayed Operation log") - salt, err := am.db.Get([]byte(saltKey), nil) - if err != nil { - if err == leveldb.ErrNotFound { - return err - } - return fmt.Errorf("failed to read salt from db: %w", err) - } - - decryptedPubKey, err := decrypt(am.encryptionKey, salt, pubKeyBz) - if err != nil { - return err - } - - decryptedPrivateKey, err := decrypt(am.encryptionKey, salt, privateKeyBz) - if err != nil { - return err - } - - am.pubKey = am.suite.Point() - if err = am.pubKey.UnmarshalBinary(decryptedPubKey); err != nil { - return fmt.Errorf("failed to unmarshal public key: %w", err) - } - - am.secKey = am.suite.Scalar() - if err = am.secKey.UnmarshalBinary(decryptedPrivateKey); err != nil { - return fmt.Errorf("failed to unmarshal private key: %w", err) - } return nil } -// SaveKeysToDB save DKG keys to LevelDB -func (am *AirgappedMachine) SaveKeysToDB() error { - pubKeyBz, err := am.pubKey.MarshalBinary() - if err != nil { - return fmt.Errorf("failed to marshal pub key: %w", err) - } - privateKeyBz, err := am.secKey.MarshalBinary() - if err != nil { - return fmt.Errorf("failed to marshal private key: %w", err) - } - - salt := make([]byte, 32) - if _, err := rand.Read(salt); err != nil { - return fmt.Errorf("failed to generate salt: %w", err) - } - - encryptedPubKey, err := encrypt(am.encryptionKey, salt, pubKeyBz) - if err != nil { - return err - } - encryptedPrivateKey, err := encrypt(am.encryptionKey, salt, privateKeyBz) - if err != nil { - return err - } - - tx, err := am.db.OpenTransaction() - if err != nil { - return fmt.Errorf("failed to open transcation for db: %w", err) - } - defer tx.Discard() - - if err = tx.Put([]byte(pubKeyDBKey), encryptedPubKey, nil); err != nil { - return fmt.Errorf("failed to put pub key into db: %w", err) - } - - if err = tx.Put([]byte(privateKeyDBKey), encryptedPrivateKey, nil); err != nil { - return fmt.Errorf("failed to put private key into db: %w", err) - } - - if err = tx.Put([]byte(saltKey), salt, nil); err != nil { - return fmt.Errorf("failed to put salt into db: %w", err) - } - - if err = tx.Commit(); err != nil { - return fmt.Errorf("failed to commit tx for saving keys into db: %w", err) - } - return nil +func (am *AirgappedMachine) DropOperationsLog(dkgIdentifier string) error { + return am.dropRoundOperationLog(dkgIdentifier) } // getParticipantID returns our own participant id for the given DKG round @@ -329,7 +161,7 @@ func (am *AirgappedMachine) encryptDataForParticipant(dkgIdentifier, to string, return nil, fmt.Errorf("failed to get pk for participant %s: %w", to, err) } - encryptedData, err := ecies.Encrypt(am.suite, pk, data, am.suite.Hash) + encryptedData, err := ecies.Encrypt(am.baseSuite, pk, data, am.baseSuite.Hash) if err != nil { return nil, fmt.Errorf("failed to encrypt data: %w", err) } @@ -338,7 +170,7 @@ func (am *AirgappedMachine) encryptDataForParticipant(dkgIdentifier, to string, // decryptDataFromParticipant decrypts the data that was sent to us func (am *AirgappedMachine) decryptDataFromParticipant(data []byte) ([]byte, error) { - decryptedData, err := ecies.Decrypt(am.suite, am.secKey, data, am.suite.Hash) + decryptedData, err := ecies.Decrypt(am.baseSuite, am.secKey, data, am.baseSuite.Hash) if err != nil { return nil, fmt.Errorf("failed to decrypt data: %w", err) } diff --git a/airgapped/airgapped_test.go b/airgapped/airgapped_test.go index 5cb6fa9..06c2d3d 100644 --- a/airgapped/airgapped_test.go +++ b/airgapped/airgapped_test.go @@ -327,7 +327,6 @@ func TestAirgappedMachine_Replay(t *testing.T) { if err != nil { t.Fatalf("failed to create airgapped machine: %v", err) } - am.SetAddress(participants[i]) am.SetEncryptionKey([]byte(fmt.Sprintf(testDB+"%d", i))) if err = am.InitKeys(); err != nil { t.Fatalf(err.Error()) @@ -432,7 +431,6 @@ func TestAirgappedMachine_Replay(t *testing.T) { if err != nil { t.Fatalf("failed to create airgapped machine: %v", err) } - _ = am.SetAddress(participants[i]) am.SetEncryptionKey([]byte(fmt.Sprintf(testDB+"%d", i))) if err = am.InitKeys(); err != nil { t.Fatalf(err.Error()) @@ -448,7 +446,7 @@ func TestAirgappedMachine_Replay(t *testing.T) { defer os.RemoveAll(testDir) for _, node := range newTr.nodes { - err := node.Machine.ReplayOperationsLog() + err := node.Machine.ReplayOperationsLog(DKGIdentifier) require.NoError(t, err) } diff --git a/airgapped/bls.go b/airgapped/bls.go index ab8bbc7..90526fa 100644 --- a/airgapped/bls.go +++ b/airgapped/bls.go @@ -118,7 +118,7 @@ func (am *AirgappedMachine) createPartialSign(msg []byte, dkgIdentifier string) return nil, fmt.Errorf("failed to load blsKeyring: %w", err) } - return tbls.Sign(am.suite.(pairing.Suite), blsKeyring.Share, msg) + return tbls.Sign(am.baseSuite.(pairing.Suite), blsKeyring.Share, msg) } // recoverFullSign recovers full threshold signature for a message @@ -129,7 +129,7 @@ func (am *AirgappedMachine) recoverFullSign(msg []byte, sigShares [][]byte, t, n return nil, fmt.Errorf("failed to load blsKeyring: %w", err) } - return tbls.Recover(am.suite.(pairing.Suite), blsKeyring.PubPoly, msg, sigShares, t, n) + return tbls.Recover(am.baseSuite.(pairing.Suite), blsKeyring.PubPoly, msg, sigShares, t, n) } // verifySign verifies a signature of a message @@ -139,5 +139,5 @@ func (am *AirgappedMachine) verifySign(msg []byte, fullSignature []byte, dkgIden return fmt.Errorf("failed to load blsKeyring: %w", err) } - return bls.Verify(am.suite.(pairing.Suite), blsKeyring.PubPoly.Commit(), msg, fullSignature) + return bls.Verify(am.baseSuite.(pairing.Suite), blsKeyring.PubPoly.Commit(), msg, fullSignature) } diff --git a/airgapped/dkg.go b/airgapped/dkg.go index 6dde039..8255368 100644 --- a/airgapped/dkg.go +++ b/airgapped/dkg.go @@ -1,9 +1,12 @@ package airgapped import ( + "crypto/sha256" "encoding/json" "fmt" + bls "github.com/depools/kyber-bls12381" + "github.com/corestario/kyber" dkgPedersen "github.com/corestario/kyber/share/dkg/pedersen" client "github.com/depools/dc4bc/client/types" @@ -42,7 +45,7 @@ func (am *AirgappedMachine) handleStateAwaitParticipantsConfirmations(o *client. pid := -1 for _, r := range payload { - pubkey := am.suite.Point() + pubkey := am.baseSuite.Point() if err := pubkey.UnmarshalBinary(r.DkgPubKey); err != nil { return fmt.Errorf("failed to unmarshal dkg pubkey: %w", err) } @@ -59,11 +62,17 @@ func (am *AirgappedMachine) handleStateAwaitParticipantsConfirmations(o *client. return fmt.Errorf("dkg instance %s already exists", o.DKGIdentifier) } - dkgInstance := dkg.Init(am.suite, am.pubKey, am.secKey) + // Here we create a new seeded suite for the new DKG round with seed = + // sha256.Sum256(baseSeed + DKGIdentifier). We need this to avoid identical + // DKG rounds. + var ( + dkgSeed = sha256.Sum256(append([]byte(o.DKGIdentifier), am.baseSeed...)) + suite = bls.NewBLS12381Suite(dkgSeed[:]) + ) + dkgInstance := dkg.Init(suite, am.pubKey, am.secKey) dkgInstance.Threshold = payload[0].Threshold //same for everyone dkgInstance.N = len(payload) am.dkgInstances[o.DKGIdentifier] = dkgInstance - req := requests.SignatureProposalParticipantRequest{ ParticipantId: pid, CreatedAt: o.CreatedAt, @@ -100,14 +109,14 @@ func (am *AirgappedMachine) handleStateDkgCommitsAwaitConfirmations(o *client.Op } for _, entry := range payload { - pubKey := am.suite.Point() + pubKey := am.baseSuite.Point() if err = pubKey.UnmarshalBinary(entry.DkgPubKey); err != nil { return fmt.Errorf("failed to unmarshal pubkey: %w", err) } dkgInstance.StorePubKey(entry.Addr, entry.ParticipantId, pubKey) } - if err = dkgInstance.InitDKGInstance(am.seed); err != nil { + if err = dkgInstance.InitDKGInstance(am.baseSeed); err != nil { return fmt.Errorf("failed to init dkg instance: %w", err) } @@ -164,7 +173,7 @@ func (am *AirgappedMachine) handleStateDkgDealsAwaitConfirmations(o *client.Oper } dkgCommits := make([]kyber.Point, 0, len(commitsBz)) for _, commitBz := range commitsBz { - commit := am.suite.Point() + commit := am.baseSuite.Point() if err = commit.UnmarshalBinary(commitBz); err != nil { return fmt.Errorf("failed to unmarshal commit: %w", err) } diff --git a/airgapped/storage.go b/airgapped/storage.go new file mode 100644 index 0000000..0151a29 --- /dev/null +++ b/airgapped/storage.go @@ -0,0 +1,242 @@ +package airgapped + +import ( + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "log" + + bls12381 "github.com/depools/kyber-bls12381" + + client "github.com/depools/dc4bc/client/types" + "github.com/syndtr/goleveldb/leveldb" +) + +const ( + pubKeyDBKey = "public_key" + privateKeyDBKey = "private_key" + saltDBKey = "salt_key" + baseSeedKey = "base_seed_key" + operationsLogDBKey = "operations_log" +) + +type RoundOperationLog map[string][]client.Operation + +func (am *AirgappedMachine) loadBaseSeed() error { + seed, err := am.getBaseSeed() + if errors.Is(err, leveldb.ErrNotFound) { + log.Println("Base seed not initialized, generating a new one...") + seed = make([]byte, seedSize) + _, err = rand.Read(seed) + if err != nil { + return fmt.Errorf("failed to rand.Read: %w", err) + } + + if err := am.storeBaseSeed(seed); err != nil { + return fmt.Errorf("failed to storeBaseSeed: %w", err) + } + + log.Println("Successfully generated a new seed") + } else if err != nil { + return fmt.Errorf("failed to getBaseSeed: %w", err) + } + + am.baseSeed = seed + am.baseSuite = bls12381.NewBLS12381Suite(am.baseSeed) + + return nil +} + +func (am *AirgappedMachine) storeBaseSeed(seed []byte) error { + if err := am.db.Put([]byte(baseSeedKey), seed, nil); err != nil { + return fmt.Errorf("failed to put baseSeed: %w", err) + } + + return nil +} + +func (am *AirgappedMachine) getBaseSeed() ([]byte, error) { + seed, err := am.db.Get([]byte(baseSeedKey), nil) + if err != nil { + return nil, fmt.Errorf("failed to get baseSeed: %w", err) + } + + return seed, nil +} + +func (am *AirgappedMachine) storeOperation(o client.Operation) error { + roundOperationsLog, err := am.getRoundOperationLog() + if err != nil { + if err == leveldb.ErrNotFound { + return err + } + return fmt.Errorf("failed to get operationsLogBz from db: %w", err) + } + + operationsLog := roundOperationsLog[o.DKGIdentifier] + operationsLog = append(operationsLog, o) + roundOperationsLog[o.DKGIdentifier] = operationsLog + + roundOperationsLogBz, err := json.Marshal(roundOperationsLog) + if err != nil { + return fmt.Errorf("failed to marshal operationsLog: %w", err) + } + + if err := am.db.Put([]byte(operationsLogDBKey), roundOperationsLogBz, nil); err != nil { + return fmt.Errorf("failed to put updated operationsLog: %w", err) + } + + return nil +} + +func (am *AirgappedMachine) getOperationsLog(dkgIdentifier string) ([]client.Operation, error) { + roundOperationsLog, err := am.getRoundOperationLog() + if err != nil { + if err == leveldb.ErrNotFound { + return nil, err + } + return nil, fmt.Errorf("failed to get operationsLogBz from db: %w", err) + } + + operationsLog, ok := roundOperationsLog[dkgIdentifier] + if !ok { + return nil, fmt.Errorf("operation log not found for %s", dkgIdentifier) + } + + return operationsLog, nil +} + +func (am *AirgappedMachine) dropRoundOperationLog(dkgIdentifier string) error { + roundOperationsLog, err := am.getRoundOperationLog() + if err != nil { + if err == leveldb.ErrNotFound { + return err + } + return fmt.Errorf("failed to get operationsLogBz from db: %w", err) + } + + roundOperationsLog[dkgIdentifier] = []client.Operation{} + roundOperationsLogBz, err := json.Marshal(roundOperationsLog) + if err != nil { + return fmt.Errorf("failed to marshal operationsLog: %w", err) + } + + if err := am.db.Put([]byte(operationsLogDBKey), roundOperationsLogBz, nil); err != nil { + return fmt.Errorf("failed to put updated operationsLog: %w", err) + } + + return nil +} + +func (am *AirgappedMachine) getRoundOperationLog() (RoundOperationLog, error) { + operationsLogBz, err := am.db.Get([]byte(operationsLogDBKey), nil) + if err != nil { + return nil, err + } + + var roundOperationsLog RoundOperationLog + if err := json.Unmarshal(operationsLogBz, &roundOperationsLog); err != nil { + return nil, fmt.Errorf("failed to unmarshal stored operationsLog: %w", err) + } + + return roundOperationsLog, nil +} + +// LoadKeysFromDB load DKG keys from LevelDB +func (am *AirgappedMachine) LoadKeysFromDB() error { + pubKeyBz, err := am.db.Get([]byte(pubKeyDBKey), nil) + if err != nil { + if err == leveldb.ErrNotFound { + return err + } + return fmt.Errorf("failed to get public key from db: %w", err) + } + + privateKeyBz, err := am.db.Get([]byte(privateKeyDBKey), nil) + if err != nil { + if err == leveldb.ErrNotFound { + return err + } + return fmt.Errorf("failed to get private key from db: %w", err) + } + + salt, err := am.db.Get([]byte(saltDBKey), nil) + if err != nil { + if err == leveldb.ErrNotFound { + return err + } + return fmt.Errorf("failed to read salt from db: %w", err) + } + + decryptedPubKey, err := decrypt(am.encryptionKey, salt, pubKeyBz) + if err != nil { + return err + } + + decryptedPrivateKey, err := decrypt(am.encryptionKey, salt, privateKeyBz) + if err != nil { + return err + } + + am.pubKey = am.baseSuite.Point() + if err = am.pubKey.UnmarshalBinary(decryptedPubKey); err != nil { + return fmt.Errorf("failed to unmarshal public key: %w", err) + } + + am.secKey = am.baseSuite.Scalar() + if err = am.secKey.UnmarshalBinary(decryptedPrivateKey); err != nil { + return fmt.Errorf("failed to unmarshal private key: %w", err) + } + return nil +} + +// SaveKeysToDB save DKG keys to LevelDB +func (am *AirgappedMachine) SaveKeysToDB() error { + pubKeyBz, err := am.pubKey.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal pub key: %w", err) + } + privateKeyBz, err := am.secKey.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal private key: %w", err) + } + + salt := make([]byte, 32) + if _, err := rand.Read(salt); err != nil { + return fmt.Errorf("failed to generate salt: %w", err) + } + + encryptedPubKey, err := encrypt(am.encryptionKey, salt, pubKeyBz) + if err != nil { + return err + } + encryptedPrivateKey, err := encrypt(am.encryptionKey, salt, privateKeyBz) + if err != nil { + return err + } + + tx, err := am.db.OpenTransaction() + if err != nil { + return fmt.Errorf("failed to open transcation for db: %w", err) + } + defer tx.Discard() + + if err = tx.Put([]byte(pubKeyDBKey), encryptedPubKey, nil); err != nil { + return fmt.Errorf("failed to put pub key into db: %w", err) + } + + if err = tx.Put([]byte(privateKeyDBKey), encryptedPrivateKey, nil); err != nil { + return fmt.Errorf("failed to put private key into db: %w", err) + } + + if err = tx.Put([]byte(saltDBKey), salt, nil); err != nil { + return fmt.Errorf("failed to put salt into db: %w", err) + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("failed to commit tx for saving keys into db: %w", err) + } + + return nil +} diff --git a/airgapped/types.go b/airgapped/types.go index 638e3a3..70aafb1 100644 --- a/airgapped/types.go +++ b/airgapped/types.go @@ -17,7 +17,7 @@ func makeBLSKeyKeyringDBKey(key string) string { } func (am *AirgappedMachine) saveBLSKeyring(dkgID string, blsKeyring *dkg.BLSKeyring) error { - salt, err := am.db.Get([]byte(saltKey), nil) + salt, err := am.db.Get([]byte(saltDBKey), nil) if err != nil { return fmt.Errorf("failed to read salt from db: %w", err) } @@ -44,7 +44,7 @@ func (am *AirgappedMachine) loadBLSKeyring(dkgID string) (*dkg.BLSKeyring, error err error ) - salt, err := am.db.Get([]byte(saltKey), nil) + salt, err := am.db.Get([]byte(saltDBKey), nil) if err != nil { return nil, fmt.Errorf("failed to read salt from db: %w", err) } @@ -58,7 +58,7 @@ func (am *AirgappedMachine) loadBLSKeyring(dkgID string) (*dkg.BLSKeyring, error return nil, fmt.Errorf("failed to decrypt BLS keyring: %w", err) } - if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.suite, decryptedKeyring); err != nil { + if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.baseSuite, decryptedKeyring); err != nil { return nil, fmt.Errorf("failed to decode bls keyring") } return blsKeyring, nil @@ -70,7 +70,7 @@ func (am *AirgappedMachine) GetBLSKeyrings() (map[string]*dkg.BLSKeyring, error) err error ) - salt, err := am.db.Get([]byte(saltKey), nil) + salt, err := am.db.Get([]byte(saltDBKey), nil) if err != nil { return nil, fmt.Errorf("failed to read salt from db: %w", err) } @@ -86,7 +86,7 @@ func (am *AirgappedMachine) GetBLSKeyrings() (map[string]*dkg.BLSKeyring, error) if err != nil { return nil, fmt.Errorf("failed to decrypt BLS keyring: %w", err) } - if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.suite, decryptedKeyring); err != nil { + if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.baseSuite, decryptedKeyring); err != nil { return nil, fmt.Errorf("failed to decode bls keyring: %w", err) } keyrings[strings.TrimLeft(string(key), blsKeyringPrefix)] = blsKeyring diff --git a/client/flow_test.go b/client/flow_test.go index e7007a0..da55662 100644 --- a/client/flow_test.go +++ b/client/flow_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/md5" + "crypto/rand" "encoding/json" "fmt" "io/ioutil" @@ -110,7 +111,9 @@ func (n *node) run(t *testing.T) { if err = json.Unmarshal(msg.Data, &pubKeyReq); err != nil { t.Fatalf("failed to unmarshal pubKey request: %v", err) } - pubKey := bls12381.NewBLS12381Suite().Point() + seed := make([]byte, 32) + _, _ = rand.Read(seed) + pubKey := bls12381.NewBLS12381Suite(seed).Point() if err = pubKey.UnmarshalBinary(pubKeyReq.MasterKey); err != nil { t.Fatalf("failed to unmarshal pubkey: %v", err) } diff --git a/cmd/airgapped/main.go b/cmd/airgapped/main.go index ec83f6b..f5d222c 100644 --- a/cmd/airgapped/main.go +++ b/cmd/airgapped/main.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "os" + "os/signal" "runtime" "strings" "syscall" @@ -52,6 +53,21 @@ func NewTerminal(machine *airgapped.AirgappedMachine) *terminal { commandHandler: t.showFinishedDKGCommand, description: "shows a list of finished dkg rounds", }) + t.addCommand("replay_operations_log", &terminalCommand{ + commandHandler: t.replayOperationLogCommand, + description: "replays the operation log for a given dkg round", + }) + t.addCommand("drop_operations_log", &terminalCommand{ + commandHandler: t.dropOperationLogCommand, + description: "drops the operation log for a given dkg round", + }) + t.addCommand("exit", &terminalCommand{ + commandHandler: func() error { + log.Fatal("interrupted") + return nil + }, + description: "stops the machine", + }) return &t } @@ -104,6 +120,32 @@ func (t *terminal) showFinishedDKGCommand() error { return nil } +func (t *terminal) replayOperationLogCommand() error { + fmt.Print("> Enter the DKGRoundIdentifier: ") + dkgRoundIdentifier, err := t.reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read dkgRoundIdentifier: %w", err) + } + + if err := t.airgapped.ReplayOperationsLog(dkgRoundIdentifier); err != nil { + return fmt.Errorf("failed to ReplayOperationsLog: %w", err) + } + return nil +} + +func (t *terminal) dropOperationLogCommand() error { + fmt.Print("> Enter the DKGRoundIdentifier: ") + dkgRoundIdentifier, err := t.reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read dkgRoundIdentifier: %w", err) + } + + if err := t.airgapped.DropOperationsLog(dkgRoundIdentifier); err != nil { + return fmt.Errorf("failed to DropOperationsLog: %w", err) + } + return nil +} + func (t *terminal) enterEncryptionPasswordIfNeeded() error { t.airgapped.Lock() defer t.airgapped.Unlock() @@ -153,7 +195,7 @@ func (t *terminal) run() error { } t.airgapped.Lock() if err := handler.commandHandler(); err != nil { - fmt.Printf("failled to execute command %s: %v, \n", command, err) + fmt.Printf("failled to execute command %s: %v \n", command, err) t.airgapped.Unlock() continue } @@ -194,6 +236,14 @@ func main() { log.Fatalf("failed to init airgapped machine %v", err) } + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for _ = range c { + fmt.Printf("Intercepting SIGINT, please type `exit` to stop the machine\n>>> ") + } + }() + t := NewTerminal(air) go t.dropSensitiveData(passwordLifeDuration) if err = t.run(); err != nil {