gnunet-go

GNUnet Bindings for Go
Log | Files | Refs | README | LICENSE

commit fdf23180919ec42422694f1f1801eead1ea576e8
parent 3d0b7cab3e273dd2f3f3d8990fb868b93b6b3f95
Author: Bernd Fix <brf@hoi-polloi.org>
Date:   Sun, 24 May 2020 13:34:05 +0200

Milestone #3 (RC1)

Diffstat:
Asrc/cmd/gnunet-service-revocation-go/main.go | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/cmd/pow-test/main.go | 51---------------------------------------------------
Asrc/cmd/revoke-zonekey/main.go | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/config/config.go | 18++++++++++++++----
Msrc/gnunet/message/factory.go | 2+-
Msrc/gnunet/message/msg_revocation.go | 35++++++++++++++---------------------
Msrc/gnunet/modules.go | 19+++++++++++++------
Msrc/gnunet/service/gns/module.go | 21++++++++++++++++++---
Msrc/gnunet/service/gns/service.go | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/service/revocation/module.go | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/service/revocation/pow.go | 346++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Asrc/gnunet/service/revocation/service.go | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/util/database.go | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/gnunet/util/key_value_store.go | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/gnunet/util/time.go | 12++++++++++++
15 files changed, 1172 insertions(+), 232 deletions(-)

diff --git a/src/cmd/gnunet-service-revocation-go/main.go b/src/cmd/gnunet-service-revocation-go/main.go @@ -0,0 +1,105 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 Bernd Fix >Y< +// +// gnunet-go is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// gnunet-go 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 +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package main + +import ( + "flag" + "os" + "os/signal" + "syscall" + "time" + + "github.com/bfix/gospel/logger" + "gnunet/config" + "gnunet/service" + "gnunet/service/revocation" +) + +func main() { + defer func() { + logger.Println(logger.INFO, "[revocation] Bye.") + // flush last messages + logger.Flush() + }() + logger.Println(logger.INFO, "[revocation] Starting service...") + + var ( + cfgFile string + srvEndp string + err error + logLevel int + ) + // handle command line arguments + flag.StringVar(&cfgFile, "c", "gnunet-config.json", "GNUnet configuration file") + flag.StringVar(&srvEndp, "s", "", "REVOCATION service end-point") + flag.IntVar(&logLevel, "L", logger.INFO, "REVOCATION log level (default: INFO)") + flag.Parse() + + // read configuration file and set missing arguments. + if err = config.ParseConfig(cfgFile); err != nil { + logger.Printf(logger.ERROR, "[revocation] Invalid configuration file: %s\n", err.Error()) + return + } + + // apply configuration + logger.SetLogLevel(logLevel) + if len(srvEndp) == 0 { + srvEndp = config.Cfg.GNS.Endpoint + } + + // start a new REVOCATION service + rvc := revocation.NewRevocationService() + srv := service.NewServiceImpl("revocation", rvc) + if err = srv.Start(srvEndp); err != nil { + logger.Printf(logger.ERROR, "[revocation] Error: '%s'\n", err.Error()) + return + } + + // handle OS signals + sigCh := make(chan os.Signal, 5) + signal.Notify(sigCh) + + // heart beat + tick := time.NewTicker(5 * time.Minute) + +loop: + for { + select { + // handle OS signals + case sig := <-sigCh: + switch sig { + case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: + logger.Printf(logger.INFO, "[revocation] Terminating service (on signal '%s')\n", sig) + break loop + case syscall.SIGHUP: + logger.Println(logger.INFO, "[revocation] SIGHUP") + case syscall.SIGURG: + // TODO: https://github.com/golang/go/issues/37942 + default: + logger.Println(logger.INFO, "[revocation] Unhandled signal: "+sig.String()) + } + // handle heart beat + case now := <-tick.C: + logger.Println(logger.INFO, "[revocation] Heart beat at "+now.String()) + } + } + + // terminating service + srv.Stop() +} diff --git a/src/cmd/pow-test/main.go b/src/cmd/pow-test/main.go @@ -1,51 +0,0 @@ -package main - -import ( - "encoding/hex" - "flag" - "fmt" - "log" - - "gnunet/service/revocation" - - "github.com/bfix/gospel/crypto/ed25519" - "github.com/bfix/gospel/math" -) - -func main() { - var ( - quiet bool - bits int - ) - flag.IntVar(&bits, "b", 25, "Number of leading zero bits") - flag.BoolVar(&quiet, "q", false, "Be quiet") - flag.Parse() - - // pre-set difficulty - fmt.Printf("Leading zeros required: %d\n", bits) - difficulty := math.TWO.Pow(512 - bits).Sub(math.ONE) - fmt.Printf("==> Difficulty: %v\n", difficulty) - - // generate a random key pair - pkey, _ := ed25519.NewKeypair() - - // initialize RevData structure - rd := revocation.NewRevData(0, pkey) - - var count uint64 = 0 - for { - result, err := rd.Compute() - if err != nil { - log.Fatal(err) - } - //fmt.Printf("Nonce=%d, Result=(%d) %v\n", rd.GetNonce(), result.BitLen(), result) - if result.Cmp(difficulty) < 0 { - break - } - count++ - rd.Next() - } - fmt.Printf("PoW found after %d iterations:\n", count) - fmt.Printf("--> Nonce=%d\n", rd.GetNonce()) - fmt.Printf(" REV = %s\n", hex.EncodeToString(rd.GetBlob())) -} diff --git a/src/cmd/revoke-zonekey/main.go b/src/cmd/revoke-zonekey/main.go @@ -0,0 +1,192 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 Bernd Fix >Y< +// +// gnunet-go is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// gnunet-go 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 +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package main + +import ( + "context" + "encoding/hex" + "flag" + "log" + "os" + "os/signal" + "sync" + "syscall" + + "gnunet/service/revocation" + "gnunet/util" + + "github.com/bfix/gospel/crypto/ed25519" + "github.com/bfix/gospel/data" +) + +func main() { + log.Println("*** Compute revocation data for a zone key") + log.Println("*** Copyright (c) 2020, Bernd Fix >Y<") + log.Println("*** This is free software distributed under the Affero GPL v3.") + + // handle command line arguments + var ( + verbose bool // be verbose with messages + bits int // number of leading zero-bit requested + zonekey string // zonekey to be revoked + filename string // name of file for persistance + ) + flag.IntVar(&bits, "b", 25, "Number of leading zero bits") + flag.BoolVar(&verbose, "v", false, "verbose output") + flag.StringVar(&zonekey, "z", "", "Zone key to be revoked") + flag.StringVar(&filename, "f", "", "Name of file to store revocation") + flag.Parse() + + // define layout of persistant data + var revData struct { + Rd *revocation.RevData // Revocation data + T util.RelativeTime // time spend in calculations + Last uint64 // last value used for PoW test + Numbits uint8 // number of leading zero-bits + } + dataBuf := make([]byte, 377) + + // read revocation object from file + file, err := os.Open(filename) + cont := true + if err != nil { + if len(zonekey) != 52 { + log.Fatal("Missing or invalid zonekey and no file specified -- aborting") + } + keyData, err := util.DecodeStringToBinary(zonekey, 32) + if err != nil { + log.Fatal("Invalid zonekey: " + err.Error()) + } + pkey := ed25519.NewPublicKeyFromBytes(keyData) + revData.Rd = revocation.NewRevData(util.AbsoluteTimeNow(), pkey) + revData.Numbits = uint8(bits) + revData.T = util.NewRelativeTime(0) + cont = false + } else { + n, err := file.Read(dataBuf) + if err != nil { + log.Fatal("Error reading file: " + err.Error()) + } + if n != len(dataBuf) { + log.Fatal("File corrupted -- aborting") + } + if err = data.Unmarshal(&revData, dataBuf); err != nil { + log.Fatal("File corrupted: " + err.Error()) + } + bits = int(revData.Numbits) + if err = file.Close(); err != nil { + log.Fatal("Error closing file: " + err.Error()) + } + } + + if cont { + log.Printf("Revocation calculation started at %s\n", revData.Rd.Timestamp.String()) + log.Printf("Time spent on calculation: %s\n", revData.T.String()) + log.Printf("Last tested PoW value: %d\n", revData.Last) + log.Println("Continuing...") + } else { + log.Println("Starting new revocation calculation...") + } + log.Println("Press ^C to abort...") + + // pre-set difficulty + log.Printf("Difficulty: %d\n", bits) + if bits < 25 { + log.Println("WARNING: difficulty is less than 25!") + } + + // Start or continue calculation + startTime := util.AbsoluteTimeNow() + ctx, cancelFcn := context.WithCancel(context.Background()) + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + defer wg.Done() + if result, last := revData.Rd.Compute(ctx, bits, revData.Last); result != 32 { + log.Printf("Incomplete revocation: Only %d of 32 PoWs available!\n", result) + revData.Last = last + revData.T = util.AbsoluteTimeNow().Diff(startTime) + log.Println("Writing revocation data to file...") + file, err := os.Create(filename) + if err != nil { + log.Fatal("Can't write to output file: " + err.Error()) + } + buf, err := data.Marshal(&revData) + if err != nil { + log.Fatal("Internal error: " + err.Error()) + } + if len(buf) != len(dataBuf) { + log.Fatal("Internal error: Buffer mismatch") + } + n, err := file.Write(buf) + if err != nil { + log.Fatal("Can't write to output file: " + err.Error()) + } + if n != len(dataBuf) { + log.Fatal("Can't write data to output file!") + } + if err = file.Close(); err != nil { + log.Fatal("Error closing file: " + err.Error()) + } + } else { + log.Println("Revocation data object:") + log.Println(" 0x" + hex.EncodeToString(revData.Rd.Blob())) + log.Println("Status:") + rc := revData.Rd.Verify() + switch { + case rc == -1: + log.Println(" Missing/invalid signature") + case rc == -2: + log.Println(" Expired revocation") + case rc == -3: + log.Println(" Wrong PoW sequence order") + case rc < 25: + log.Println(" Difficulty to small") + default: + log.Printf(" Difficulty: %d\n", rc) + } + } + }() + + go func() { + // handle OS signals + sigCh := make(chan os.Signal, 5) + signal.Notify(sigCh) + loop: + for { + select { + // handle OS signals + case sig := <-sigCh: + switch sig { + case syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM: + log.Printf("Terminating (on signal '%s')\n", sig) + cancelFcn() + break loop + case syscall.SIGHUP: + log.Println("SIGHUP") + case syscall.SIGURG: + // TODO: https://github.com/golang/go/issues/37942 + default: + log.Println("Unhandled signal: " + sig.String()) + } + } + } + }() + wg.Wait() +} diff --git a/src/gnunet/config/config.go b/src/gnunet/config/config.go @@ -55,16 +55,26 @@ type NamecacheConfig struct { } /////////////////////////////////////////////////////////////////////// +// Revocation configuration + +// RevocationConfig +type RevocationConfig struct { + Endpoint string `json:"endpoint"` // end-point of Revocation service + Storage string `json:"storage"` // persistance mechanism for revocation data +} + +/////////////////////////////////////////////////////////////////////// // Environment settings type Environ map[string]string // Config is the aggregated configuration for GNUnet. type Config struct { - Env Environ `json:"environ"` - DHT *DHTConfig `json:"dht"` - GNS *GNSConfig `json:"gns"` - Namecache *NamecacheConfig `json:"namecache"` + Env Environ `json:"environ"` + DHT *DHTConfig `json:"dht"` + GNS *GNSConfig `json:"gns"` + Namecache *NamecacheConfig `json:"namecache"` + Revocation *RevocationConfig `json:"revocation"` } var ( diff --git a/src/gnunet/message/factory.go b/src/gnunet/message/factory.go @@ -92,7 +92,7 @@ func NewEmptyMessage(msgType uint16) (Message, error) { case REVOCATION_QUERY_RESPONSE: return NewRevocationQueryResponseMsg(true), nil case REVOCATION_REVOKE: - return NewRevocationRevokeMsg(0, nil, nil), nil + return NewRevocationRevokeMsg(nil, nil), nil case REVOCATION_REVOKE_RESPONSE: return NewRevocationRevokeResponseMsg(false), nil } diff --git a/src/gnunet/message/msg_revocation.go b/src/gnunet/message/msg_revocation.go @@ -21,8 +21,6 @@ package message import ( "fmt" - "gnunet/crypto" - "gnunet/enums" "gnunet/util" "github.com/bfix/gospel/crypto/ed25519" @@ -104,28 +102,23 @@ func (msg *RevocationQueryResponseMsg) Header() *MessageHeader { // RevocationRevokeMsg type RevocationRevokeMsg struct { - MsgSize uint16 `order:"big"` // total size of message - MsgType uint16 `order:"big"` // REVOCATION_QUERY (636) - Reserved uint32 `order:"big"` // Reserved for future use - PoW uint64 `order:"big"` // Proof-of-work: nonce that satisfy condition - Signature []byte `size:"64"` // Signature of the revocation. - Purpose *crypto.SignaturePurpose // Size and purpose of signature (8 bytes) - ZoneKey []byte `size:"32"` // Zone key to be revoked + MsgSize uint16 `order:"big"` // total size of message + MsgType uint16 `order:"big"` // REVOCATION_REVOKE (638) + Timestamp util.AbsoluteTime // Timestamp of revocation creation + PoWs []uint64 `size:"32" order:"big"` // (Sorted) list of PoW values + Signature []byte `size:"64"` // Signature (Proof-of-ownership). + ZoneKey []byte `size:"32"` // public zone key to be revoked } // NewRevocationRevokeMsg creates a new message for a given zone. -func NewRevocationRevokeMsg(pow uint64, zoneKey *ed25519.PublicKey, sig *ed25519.EcSignature) *RevocationRevokeMsg { +func NewRevocationRevokeMsg(zoneKey *ed25519.PublicKey, sig *ed25519.EcSignature) *RevocationRevokeMsg { msg := &RevocationRevokeMsg{ - MsgSize: 120, + MsgSize: 364, MsgType: REVOCATION_REVOKE, - Reserved: 0, - PoW: pow, + Timestamp: util.AbsoluteTimeNow(), + PoWs: make([]uint64, 32), Signature: make([]byte, 64), - Purpose: &crypto.SignaturePurpose{ - Size: 40, - Purpose: enums.SIG_REVOCATION, - }, - ZoneKey: make([]byte, 32), + ZoneKey: make([]byte, 32), } if zoneKey != nil { copy(msg.ZoneKey, zoneKey.Bytes()) @@ -138,7 +131,7 @@ func NewRevocationRevokeMsg(pow uint64, zoneKey *ed25519.PublicKey, sig *ed25519 // String returns a human-readable representation of the message. func (m *RevocationRevokeMsg) String() string { - return fmt.Sprintf("RevocationRevokeMsg{pow=%d,zone=%s}", m.PoW, util.EncodeBinaryToString(m.ZoneKey)) + return fmt.Sprintf("RevocationRevokeMsg{zone=%s}", util.EncodeBinaryToString(m.ZoneKey)) } // Header returns the message header in a separate instance. @@ -153,8 +146,8 @@ func (msg *RevocationRevokeMsg) Header() *MessageHeader { // RevocationRevokeResponseMsg type RevocationRevokeResponseMsg struct { MsgSize uint16 `order:"big"` // total size of message - MsgType uint16 `order:"big"` // REVOCATION_QUERY_RESPONSE (637) - Success uint32 `order:"big"` // Revoke successful? + MsgType uint16 `order:"big"` // REVOCATION_REVOKE_RESPONSE (639) + Success uint32 `order:"big"` // Revoke successful? (0=no, 1=yes) } // NewRevocationRevokeResponseMsg creates a new response for a query. diff --git a/src/gnunet/modules.go b/src/gnunet/modules.go @@ -32,13 +32,15 @@ import ( "gnunet/service/dht" "gnunet/service/gns" "gnunet/service/namecache" + "gnunet/service/revocation" ) // List of all GNUnet service module instances type Instances struct { - GNS *gns.GNSModule - Namecache *namecache.NamecacheModule - DHT *dht.DHTModule + GNS *gns.GNSModule + Namecache *namecache.NamecacheModule + DHT *dht.DHTModule + Revocation *revocation.RevocationModule } // Local reference to instance list @@ -55,10 +57,15 @@ func init() { // DHT (no calls to other modules) Modules.DHT = new(dht.DHTModule) + // Revocation (no calls to other modules) + Modules.Revocation = revocation.NewRevocationModule() + // GNS (calls Namecache, DHT and Identity) Modules.GNS = &gns.GNSModule{ - LookupLocal: Modules.Namecache.Get, - StoreLocal: Modules.Namecache.Put, - LookupRemote: Modules.DHT.Get, + LookupLocal: Modules.Namecache.Get, + StoreLocal: Modules.Namecache.Put, + LookupRemote: Modules.DHT.Get, + RevocationQuery: Modules.Revocation.Query, + RevocationRevoke: Modules.Revocation.Revoke, } } diff --git a/src/gnunet/service/gns/module.go b/src/gnunet/service/gns/module.go @@ -27,6 +27,7 @@ import ( "gnunet/enums" "gnunet/message" "gnunet/service" + "gnunet/service/revocation" "gnunet/util" "github.com/bfix/gospel/crypto/ed25519" @@ -111,9 +112,11 @@ func NewQuery(pkey *ed25519.PublicKey, label string) *Query { // GNSModule handles the resolution of GNS names to RRs bundled in a block. type GNSModule struct { // Use function references for calls to methods in other modules: - LookupLocal func(ctx *service.SessionContext, query *Query) (*message.GNSBlock, error) - StoreLocal func(ctx *service.SessionContext, block *message.GNSBlock) error - LookupRemote func(ctx *service.SessionContext, query *Query) (*message.GNSBlock, error) + LookupLocal func(ctx *service.SessionContext, query *Query) (*message.GNSBlock, error) + StoreLocal func(ctx *service.SessionContext, block *message.GNSBlock) error + LookupRemote func(ctx *service.SessionContext, query *Query) (*message.GNSBlock, error) + RevocationQuery func(ctx *service.SessionContext, pkey *ed25519.PublicKey) (valid bool, err error) + RevocationRevoke func(ctx *service.SessionContext, rd *revocation.RevData) (success bool, err error) } // Resolve a GNS name with multiple labels. If pkey is not nil, the name @@ -158,6 +161,12 @@ func (gns *GNSModule) ResolveAbsolute( err = ErrUnknownTLD return } + // check if zone key has been revoked + var valid bool + set = message.NewGNSRecordSet() + if valid, err = gns.RevocationQuery(ctx, pkey); err != nil || !valid { + return + } // continue with resolution relative to a zone. return gns.ResolveRelative(ctx, labels[1:], pkey, kind, mode, depth) } @@ -229,6 +238,12 @@ func (gns *GNSModule) ResolveRelative( if len(labels) == 1 && !kind.HasType(enums.GNS_TYPE_PKEY) { labels = append(labels, "@") } + // check if zone key has been revoked + if valid, err := gns.RevocationQuery(ctx, pkey); err != nil || !valid { + // revoked key -> no results! + records = make([]*message.GNSResourceRecord, 0) + break + } } else if hdlr := hdlrs.GetHandler(enums.GNS_TYPE_GNS2DNS); hdlr != nil { // (2) GNS2DNS records inst := hdlr.(*Gns2DnsHandler) diff --git a/src/gnunet/service/gns/service.go b/src/gnunet/service/gns/service.go @@ -28,6 +28,7 @@ import ( "gnunet/enums" "gnunet/message" "gnunet/service" + "gnunet/service/revocation" "gnunet/transport" "gnunet/util" @@ -59,6 +60,8 @@ func NewGNSService() service.Service { inst.LookupLocal = inst.LookupNamecache inst.StoreLocal = inst.StoreNamecache inst.LookupRemote = inst.LookupDHT + inst.RevocationQuery = inst.QueryKeyRevocation + inst.RevocationRevoke = inst.RevokeKey return inst } @@ -167,6 +170,60 @@ loop: ctx.Cancel() } +//====================================================================== + +// +func (s *GNSService) QueryKeyRevocation(ctx *service.SessionContext, pkey *ed25519.PublicKey) (valid bool, err error) { + logger.Printf(logger.DBG, "[gns] QueryKeyRev(%s)...\n", util.EncodeBinaryToString(pkey.Bytes())) + + // assemble request + req := message.NewRevocationQueryMsg(pkey) + + // get response from Revocation service + var resp message.Message + if resp, err = service.ServiceRequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Endpoint, req); err != nil { + return + } + + // handle message depending on its type + logger.Println(logger.DBG, "[gns] Handling response from Revocation service") + valid = false + switch m := resp.(type) { + case *message.RevocationQueryResponseMsg: + valid = (m.Valid == 1) + } + return +} + +// +func (s *GNSService) RevokeKey(ctx *service.SessionContext, rd *revocation.RevData) (success bool, err error) { + logger.Printf(logger.DBG, "[gns] RevokeKey(%s)...\n", util.EncodeBinaryToString(rd.ZoneKey)) + + // assemble request + req := message.NewRevocationRevokeMsg(nil, nil) + req.Timestamp = rd.Timestamp + copy(req.PoWs, rd.PoWs) + copy(req.Signature, rd.Signature) + copy(req.ZoneKey, rd.ZoneKey) + + // get response from Revocation service + var resp message.Message + if resp, err = service.ServiceRequestResponse(ctx, "gns", "Revocation", config.Cfg.Revocation.Endpoint, req); err != nil { + return + } + + // handle message depending on its type + logger.Println(logger.DBG, "[gns] Handling response from Revocation service") + success = false + switch m := resp.(type) { + case *message.RevocationRevokeResponseMsg: + success = (m.Success == 1) + } + return +} + +//====================================================================== + // LookupNamecache func (s *GNSService) LookupNamecache(ctx *service.SessionContext, query *Query) (block *message.GNSBlock, err error) { logger.Printf(logger.DBG, "[gns] LookupNamecache(%s)...\n", hex.EncodeToString(query.Key.Bits)) @@ -266,6 +323,8 @@ func (s *GNSService) StoreNamecache(ctx *service.SessionContext, block *message. return } +//====================================================================== + // LookupDHT func (s *GNSService) LookupDHT(ctx *service.SessionContext, query *Query) (block *message.GNSBlock, err error) { logger.Printf(logger.DBG, "[gns] LookupDHT(%s)...\n", hex.EncodeToString(query.Key.Bits)) diff --git a/src/gnunet/service/revocation/module.go b/src/gnunet/service/revocation/module.go @@ -0,0 +1,124 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 Bernd Fix >Y< +// +// gnunet-go is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// gnunet-go 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 +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package revocation + +import ( + "gnunet/config" + "gnunet/service" + "gnunet/util" + + "github.com/bfix/gospel/crypto/ed25519" + "github.com/bfix/gospel/data" + "github.com/bfix/gospel/logger" +) + +//====================================================================== +// "GNUnet Revocation" implementation +//====================================================================== + +// RevocationModule handles the revocation-related calls to other modules. +type RevocationModule struct { + bloomf *data.BloomFilter // bloomfilter for fast revocation check + kvs util.KeyValueStore // storage for known revocations +} + +// Init a revocation module +func (m *RevocationModule) Init() error { + // Initialize access to revocation data storage + var err error + if m.kvs, err = util.OpenKVStore(config.Cfg.Revocation.Storage); err != nil { + return err + } + // traverse the storage and build bloomfilter for all keys + m.bloomf = data.NewBloomFilter(1000000, 1e-8) + keys, err := m.kvs.List() + if err != nil { + return err + } + for _, key := range keys { + buf, err := util.DecodeStringToBinary(key, 32) + if err != nil { + return err + } + m.bloomf.Add(buf) + } + return nil +} + +// NewRevocationModule returns an initialized revocation module +func NewRevocationModule() *RevocationModule { + m := new(RevocationModule) + if err := m.Init(); err != nil { + logger.Printf(logger.ERROR, "[revocation] Failed to initialize module: %s\n", err.Error()) + return nil + } + return m +} + +// Query return true if the pkey is valid (not revoked) and false +// if the pkey has been revoked. +func (s *RevocationModule) Query(ctx *service.SessionContext, pkey *ed25519.PublicKey) (valid bool, err error) { + // fast check first: is the key in the bloomfilter? + data := pkey.Bytes() + if !s.bloomf.Contains(data) { + // no: it is valid (not revoked) + return true, nil + } + // check in store to detect false-positives + key := util.EncodeBinaryToString(data) + if _, err = s.kvs.Get(key); err != nil { + logger.Printf(logger.ERROR, "[revocation] Failed to locate key '%s' in store: %s\n", key, err.Error()) + // assume not revoked... + return true, err + } + // key seems to be revoked + return false, nil +} + +// Revoke +func (s *RevocationModule) Revoke(ctx *service.SessionContext, rd *RevData) (success bool, err error) { + // verify the revocation data + rc := rd.Verify() + switch { + case rc == -1: + logger.Println(logger.WARN, "[revocation] Revoke: Missing/invalid signature") + return false, nil + case rc == -2: + logger.Println(logger.WARN, "[revocation] Revoke: Expired revocation") + return false, nil + case rc == -3: + logger.Println(logger.WARN, "[revocation] Revoke: Wrong PoW sequence order") + return false, nil + case rc < 25: + logger.Println(logger.WARN, "[revocation] Revoke: Difficulty to small") + return false, nil + } + // store the revocation data + // (1) add it to the bloomfilter + s.bloomf.Add(rd.ZoneKey) + // (2) add it to the store + var buf []byte + key := util.EncodeBinaryToString(rd.ZoneKey) + if buf, err = data.Marshal(rd); err != nil { + return false, err + } + value := util.EncodeBinaryToString(buf) + err = s.kvs.Put(key, value) + return true, err +} diff --git a/src/gnunet/service/revocation/pow.go b/src/gnunet/service/revocation/pow.go @@ -20,71 +20,74 @@ package revocation import ( "bytes" - "crypto/cipher" - "crypto/sha256" - "crypto/sha512" + "context" "encoding/binary" - "sync" + "time" + "gnunet/crypto" + "gnunet/enums" + "gnunet/message" "gnunet/util" "github.com/bfix/gospel/crypto/ed25519" "github.com/bfix/gospel/data" "github.com/bfix/gospel/math" - "golang.org/x/crypto/hkdf" - "golang.org/x/crypto/scrypt" - "golang.org/x/crypto/twofish" + "golang.org/x/crypto/argon2" ) //---------------------------------------------------------------------- -// Revocation data +// Proof-of-Work data //---------------------------------------------------------------------- -// RevData is the revocation data structure (wire format) -type RevData struct { - Nonce uint64 `order:"big"` // start with this nonce value - ZoneKey []byte `size:"32"` // public zone key to be revoked +// PoWData is the proof-of-work data +type PoWData struct { + PoW uint64 `order:"big"` // start with this PoW value + Timestamp util.AbsoluteTime // Timestamp of creation + ZoneKey []byte `size:"32"` // public zone key to be revoked // transient attributes (not serialized) blob []byte // binary representation of serialized data } -// NewRevData creates a RevData instance for the given arguments. -func NewRevData(nonce uint64, zoneKey *ed25519.PublicKey) *RevData { - rd := &RevData{ - Nonce: nonce, - ZoneKey: make([]byte, 32), +// NewPoWData creates a PoWData instance for the given arguments. +func NewPoWData(pow uint64, ts util.AbsoluteTime, zoneKey []byte) *PoWData { + rd := &PoWData{ + PoW: 0, + Timestamp: ts, + ZoneKey: zoneKey, } - copy(rd.ZoneKey, zoneKey.Bytes()) - blob, err := data.Marshal(rd) - if err != nil { + if rd.SetPoW(pow) != nil { return nil } - rd.blob = blob return rd } -// GetNonce returns the last checked nonce value -func (r *RevData) GetNonce() uint64 { - if r.blob != nil { - var val uint64 - binary.Read(bytes.NewReader(r.blob[:8]), binary.BigEndian, &val) - r.Nonce = val +func (p *PoWData) SetPoW(pow uint64) error { + p.PoW = pow + blob, err := data.Marshal(p) + if err != nil { + return err } - return r.Nonce + p.blob = blob + return nil } -// GetBlob returns the binary representation of RevData -func (r *RevData) GetBlob() []byte { - return r.blob +// GetPoW returns the last checked PoW value +func (p *PoWData) GetPoW() uint64 { + if p.blob != nil { + var val uint64 + binary.Read(bytes.NewReader(p.blob[:8]), binary.BigEndian, &val) + p.PoW = val + } + return p.PoW } -// Next selects the next nonce to be tested. -func (r *RevData) Next() { +// Next selects the next PoW to be tested. +func (p *PoWData) Next() { var incr func(pos int) incr = func(pos int) { - r.blob[pos]++ - if r.blob[pos] != 0 || pos == 0 { + p.blob[pos]++ + if p.blob[pos] != 0 || pos == 0 { return } incr(pos - 1) @@ -92,140 +95,191 @@ func (r *RevData) Next() { incr(7) } -// Compute calculates the current result for a RevData content. +// Compute calculates the current result for a PoWData content. // The result is returned as a big integer value. -func (r *RevData) Compute() (*math.Int, error) { - - // generate key material - k, err := scrypt.Key(r.blob, []byte("gnunet-revocation-proof-of-work"), 2, 8, 2, 32) - if err != nil { - return nil, err - } - - // generate initialization vector - iv := make([]byte, 16) - prk := hkdf.Extract(sha512.New, k, []byte("gnunet-proof-of-work-iv")) - rdr := hkdf.Expand(sha256.New, prk, []byte("gnunet-revocation-proof-of-work")) - rdr.Read(iv) - - // Encrypt with Twofish CFB stream cipher - out := make([]byte, len(r.blob)) - tf, err := twofish.NewCipher(k) - if err != nil { - return nil, err - } - cipher.NewCFBEncrypter(tf, iv).XORKeyStream(out, r.blob) - - // compute result - result, err := scrypt.Key(out, []byte("gnunet-revocation-proof-of-work"), 2, 8, 2, 64) - return math.NewIntFromBytes(result), nil +func (p *PoWData) Compute() *math.Int { + key := argon2.Key(p.blob, []byte("gnunet-revocation-proof-of-work"), 3, 1024, 1, 64) + return math.NewIntFromBytes(key) } //---------------------------------------------------------------------- -// Command types for Worker +// Revocation data //---------------------------------------------------------------------- -// StartCmd starts the PoW calculation beginng at given nonce. If a -// revocation is initiated the first time, the nonce is 0. If the computation -// was interrupted (because the revocation service was shutting down), the -// computation can resume for the next unchecked nonce value. -// see: StartResponse -type StartCmd struct { - ID int // Command identifier (to relate responses) - task *RevData // RevData instance to be started -} - -// PauseCmd temporarily pauses the calculation of a PoW. -// see: PauseResponse -type PauseCmd struct { - ID int // Command identifier (to relate responses) - taskID int // identifier for PoW task -} - -// ResumeCmd resumes a paused PoW calculation. -// see: ResumeResponse -type ResumeCmd struct { - ID int // Command identifier (to relate responses) - taskID int // identifier for PoW task -} - -// BreakCmd interrupts a running PoW calculation -type BreakCmd struct { - ID int // Command identifier (to relate responses) - taskID int // identifier for PoW task +// RevData is the revocation data (wire format) +type RevData struct { + Timestamp util.AbsoluteTime // Timestamp of creation + PoWs []uint64 `size:"32" order:"big"` // (Sorted) list of PoW values + Signature []byte `size:"64"` // Signature (Proof-of-ownership). + ZoneKey []byte `size:"32"` // public zone key to be revoked } -//---------------------------------------------------------------------- -// Response types for Worker -//---------------------------------------------------------------------- - -// StartResponse is a reply to the StartCmd message -type StartResponse struct { - ID int // Command identifier (to relate responses) - taskID int // identifier for PoW task - err error // error code (nil on success) +// SignedRevData is the block of data signed for a RevData instance. +type SignedRevData struct { + Purpose *crypto.SignaturePurpose + ZoneKey []byte `size:"32"` // public zone key to be revoked + Timestamp util.AbsoluteTime // Timestamp of creation } -// PauseResponse is a reply to the PauseCmd message -type PauseResponse struct { - ID int // Command identifier (to relate responses) - err error // error code (nil on success) +// NewRevData initializes a new RevData instance +func NewRevData(ts util.AbsoluteTime, pkey *ed25519.PublicKey) *RevData { + rd := &RevData{ + Timestamp: ts, + PoWs: make([]uint64, 32), + Signature: make([]byte, 64), + ZoneKey: make([]byte, 32), + } + copy(rd.ZoneKey, pkey.Bytes()) + return rd } -// ResumeResponse is a reply to the ResumeCmd message -type ResumeResponse struct { - ID int // Command identifier (to relate responses) - err error // error code (nil on success) +// NewRevDataFromMsg initializes a new RevData instance from a GNUnet message +func NewRevDataFromMsg(m *message.RevocationRevokeMsg) *RevData { + rd := &RevData{ + Timestamp: m.Timestamp, + Signature: util.Clone(m.Signature), + ZoneKey: util.Clone(m.ZoneKey), + } + for i, pow := range m.PoWs { + rd.PoWs[i] = pow + } + return rd } -// BreakResponse is a reply to the BreakCmd message -type BreakResponse struct { - ID int // Command identifier (to relate responses) - Nonce uint64 // last checked nonce value +// Sign the revocation data +func (rd *RevData) Sign(skey *ed25519.PrivateKey) error { + sigBlock := &SignedRevData{ + Purpose: &crypto.SignaturePurpose{ + Size: 48, + Purpose: enums.SIG_REVOCATION, + }, + ZoneKey: rd.ZoneKey, + Timestamp: rd.Timestamp, + } + sigData, err := data.Marshal(sigBlock) + if err != nil { + return err + } + sig, err := skey.EcSign(sigData) + if err != nil { + return err + } + copy(rd.Signature, sig.Bytes()) + return nil } -//---------------------------------------------------------------------- -// Worker instance -//---------------------------------------------------------------------- - -// Task represents a currently active PoW calculation -type Task struct { - ID int - rev *RevData - active bool -} +// Verify a revocation object: returns the (smallest) number of leading +// zero-bits in the PoWs of this revocation; a number > 0, but smaller +// than the minimum (25) indicates invalid PoWs; a value of -1 indicates +// a failed signature; -2 indicates an expired revocation and -3 for a +// "out-of-order" PoW sequence. +func (rd *RevData) Verify() int { + + // (1) check signature + sigBlock := &SignedRevData{ + Purpose: &crypto.SignaturePurpose{ + Size: 48, + Purpose: enums.SIG_REVOCATION, + }, + ZoneKey: rd.ZoneKey, + Timestamp: rd.Timestamp, + } + sigData, err := data.Marshal(sigBlock) + if err != nil { + return -1 + } + pkey := ed25519.NewPublicKeyFromBytes(rd.ZoneKey) + sig, err := ed25519.NewEcSignatureFromBytes(rd.Signature) + if err != nil { + return -1 + } + valid, err := pkey.EcVerify(sigData, sig) + if err != nil || !valid { + return -1 + } -// Worker is the revocation worker. It is responsible to manage ad schedule -// the proof-of-work tasks for revocations. -type Worker struct { - tasks map[int]*Task - wg *sync.WaitGroup -} + // (2) check PoWs + var ( + zbits int = 512 + last uint64 = 0 + ) + for _, pow := range rd.PoWs { + // check sequence order + if pow <= last { + return -3 + } + last = pow + // compute number of leading zero-bits + work := NewPoWData(pow, rd.Timestamp, rd.ZoneKey) + lzb := 512 - work.Compute().BitLen() + if lzb < zbits { + zbits = lzb + } + } -func NewWorker() *Worker { - return &Worker{ - tasks: make(map[int]*Task), - wg: new(sync.WaitGroup), + // (3) check expiration + ttl := time.Duration((zbits-24)*365*24) * time.Hour + if util.AbsoluteTimeNow().Add(ttl).Expired() { + return -2 } + return zbits } -func (w *Worker) Run(wg *sync.WaitGroup, cmdCh chan interface{}, responseCh chan interface{}) { - defer wg.Done() - for { - select { - case cmd := <-cmdCh: - switch x := cmd.(type) { - case *StartCmd: - task := &Task{ - ID: util.NextID(), - rev: x.task, - active: true, +// Compute tries to compute a valid Revocation; it returns the number of +// solved PoWs. The computation is complete if 32 PoWs have been found. +func (rd *RevData) Compute(ctx context.Context, bits int, last uint64) (int, uint64) { + // set difficulty based on requested number of leading zero-bits + difficulty := math.TWO.Pow(512 - bits).Sub(math.ONE) + + // initialize a new work record (single PoW computation) + work := NewPoWData(0, rd.Timestamp, rd.ZoneKey) + + // work on all PoWs in a revocation data structure; make sure all PoWs + // are set to a valid value (that results in a valid compute() result + // below a given threshold) + for i, pow := range rd.PoWs { + // handle "new" pow value: set it to last_pow+1 + // this ensures a correctly sorted pow list by design. + if pow == 0 { + pow = last + } + if pow == 0 && i > 0 { + pow = rd.PoWs[i-1] + 1 + } + // prepare for PoW_i + work.SetPoW(pow) + + // Find PoW value in an (interruptable) loop + out := make(chan bool) + go func() { + for { + res := work.Compute() + if res.Cmp(difficulty) < 0 { + break } - w.tasks[task.ID] = task + work.Next() + } + out <- true + }() + loop: + for { + select { + case <-out: + rd.PoWs[i] = work.GetPoW() + break loop + case <-ctx.Done(): + return i, work.GetPoW() + 1 } - - default: - // compute a single round of currently active tasks } } + // we have found all valid PoW values. + return 32, 0 +} + +func (rd *RevData) Blob() []byte { + blob, err := data.Marshal(rd) + if err != nil { + return nil + } + return blob } diff --git a/src/gnunet/service/revocation/service.go b/src/gnunet/service/revocation/service.go @@ -0,0 +1,160 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 Bernd Fix >Y< +// +// gnunet-go is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// gnunet-go 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 +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package revocation + +import ( + "io" + + "gnunet/message" + "gnunet/service" + "gnunet/transport" + + "github.com/bfix/gospel/crypto/ed25519" + "github.com/bfix/gospel/logger" +) + +//---------------------------------------------------------------------- +// "GNUnet Revocation" service implementation +//---------------------------------------------------------------------- + +// RevocationService +type RevocationService struct { + RevocationModule +} + +// NewRevocationService +func NewRevocationService() service.Service { + // instantiate service and assemble a new Revocation handler. + inst := new(RevocationService) + return inst +} + +// Start the Revocation service +func (s *RevocationService) Start(spec string) error { + return nil +} + +// Stop the Revocation service +func (s *RevocationService) Stop() error { + return nil +} + +// Serve a client channel. +func (s *RevocationService) ServeClient(ctx *service.SessionContext, mc *transport.MsgChannel) { + + reqId := 0 +loop: + for { + // receive next message from client + reqId++ + logger.Printf(logger.DBG, "[revocation:%d:%d] Waiting for client request...\n", ctx.Id, reqId) + msg, err := mc.Receive(ctx.Signaller()) + if err != nil { + if err == io.EOF { + logger.Printf(logger.INFO, "[revocation:%d:%d] Client channel closed.\n", ctx.Id, reqId) + } else if err == transport.ErrChannelInterrupted { + logger.Printf(logger.INFO, "[revocation:%d:%d] Service operation interrupted.\n", ctx.Id, reqId) + } else { + logger.Printf(logger.ERROR, "[revocation:%d:%d] Message-receive failed: %s\n", ctx.Id, reqId, err.Error()) + } + break loop + } + logger.Printf(logger.INFO, "[revocation:%d:%d] Received request: %v\n", ctx.Id, reqId, msg) + + // handle request + switch m := msg.(type) { + case *message.RevocationQueryMsg: + //---------------------------------------------------------- + // REVOCATION_QUERY + //---------------------------------------------------------- + go func(id int, m *message.RevocationQueryMsg) { + logger.Printf(logger.INFO, "[revocation:%d:%d] Query request received.\n", ctx.Id, id) + var resp *message.RevocationQueryResponseMsg + ctx.Add() + defer func() { + // send response + if resp != nil { + if err := mc.Send(resp, ctx.Signaller()); err != nil { + logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to send response: %s\n", ctx.Id, id, err.Error()) + } + } + // go-routine finished + logger.Printf(logger.DBG, "[revocation:%d:%d] Query request finished.\n", ctx.Id, id) + ctx.Remove() + }() + + pkey := ed25519.NewPublicKeyFromBytes(m.Zone) + valid, err := s.Query(ctx, pkey) + if err != nil { + logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to query revocation status: %s\n", ctx.Id, id, err.Error()) + if err == transport.ErrChannelInterrupted { + resp = nil + } + return + } + resp = message.NewRevocationQueryResponseMsg(valid) + }(reqId, m) + + case *message.RevocationRevokeMsg: + //---------------------------------------------------------- + // REVOCATION_REVOKE + //---------------------------------------------------------- + go func(id int, m *message.RevocationRevokeMsg) { + logger.Printf(logger.INFO, "[revocation:%d:%d] Revoke request received.\n", ctx.Id, id) + var resp *message.RevocationRevokeResponseMsg + ctx.Add() + defer func() { + // send response + if resp != nil { + if err := mc.Send(resp, ctx.Signaller()); err != nil { + logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to send response: %s\n", ctx.Id, id, err.Error()) + } + } + // go-routine finished + logger.Printf(logger.DBG, "[revocation:%d:%d] Revoke request finished.\n", ctx.Id, id) + ctx.Remove() + }() + + rd := NewRevDataFromMsg(m) + valid, err := s.Revoke(ctx, rd) + if err != nil { + logger.Printf(logger.ERROR, "[revocation:%d:%d] Failed to revoke key: %s\n", ctx.Id, id, err.Error()) + if err == transport.ErrChannelInterrupted { + resp = nil + } + return + } + resp = message.NewRevocationRevokeResponseMsg(valid) + }(reqId, m) + + default: + //---------------------------------------------------------- + // UNKNOWN message type received + //---------------------------------------------------------- + logger.Printf(logger.ERROR, "[revocation:%d:%d] Unhandled message of type (%d)\n", ctx.Id, reqId, msg.Header().MsgType) + break loop + } + } + // close client connection + mc.Close() + + // cancel all tasks running for this session/connection + logger.Printf(logger.INFO, "[revocation:%d] Start closing session... [%d]\n", ctx.Id, ctx.Waiting()) + ctx.Cancel() +} diff --git a/src/gnunet/util/database.go b/src/gnunet/util/database.go @@ -0,0 +1,72 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 Bernd Fix >Y< +// +// gnunet-go is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// gnunet-go 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 +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package util + +import ( + "database/sql" + "fmt" + "os" + "strings" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/mattn/go-sqlite3" +) + +// Error messages related to databases +var ( + ErrSqlInvalidDatabaseSpec = fmt.Errorf("Invalid database specification") + ErrSqlNoDatabase = fmt.Errorf("Database not found") +) + +// ConnectSqlDatabase connects to an SQL database (various types and flavors): +// The 'spec' option defines the arguments required to connect to a database; +// the meaning and format of the arguments depends on the specific SQL database. +// The arguments are seperated by the '+' character; the first (and mandatory) +// argument defines the SQL database type. Other arguments depend on the value +// of this first argument. +// The following SQL types are implemented: +// * 'sqlite3': SQLite3-compatible database; the second argument specifies the +// file that holds the data (e.g. "sqlite3+/home/user/store.db") +// * 'mysql': A MySQL-compatible database; the second argument specifies the +// information required to log into the database (e.g. +// "[user[:passwd]@][proto[(addr)]]/dbname[?param1=value1&...]"). +func ConnectSqlDatabase(spec string) (db *sql.DB, err error) { + // split spec string into segments + specs := strings.Split(spec, ":") + if len(specs) < 2 { + return nil, ErrSqlInvalidDatabaseSpec + } + switch specs[0] { + case "sqlite3": + // check if the database file exists + var fi os.FileInfo + if fi, err = os.Stat(specs[1]); err != nil { + return nil, ErrSqlNoDatabase + } + if fi.IsDir() { + return nil, ErrSqlNoDatabase + } + // open the database file + return sql.Open("sqlite3", specs[1]) + case "mysql": + // just connect to the database + return sql.Open("mysql", specs[1]) + } + return nil, ErrSqlInvalidDatabaseSpec +} diff --git a/src/gnunet/util/key_value_store.go b/src/gnunet/util/key_value_store.go @@ -0,0 +1,188 @@ +// This file is part of gnunet-go, a GNUnet-implementation in Golang. +// Copyright (C) 2019, 2020 Bernd Fix >Y< +// +// gnunet-go is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// gnunet-go 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 +// Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// SPDX-License-Identifier: AGPL3.0-or-later + +package util + +import ( + "context" + "database/sql" + "fmt" + "strconv" + "strings" + + "github.com/go-redis/redis" +) + +// Error messages related to the key/value-store implementations +var ( + ErrKVSInvalidSpec = fmt.Errorf("Invalid KVStore specification") + ErrKVSNotAvailable = fmt.Errorf("KVStore not available") +) + +// KeyValueStore interface for implementations that store and retrieve +// key/value pairs. Keys and values are strings. +type KeyValueStore interface { + Put(key string, value string) error // put a key/value pair into store + Get(key string) (string, error) // retrieve a value for a key from store + List() ([]string, error) // get all keys from the store +} + +// OpenKVStore opens a key/value store for further put/get operations. +// The 'spec' option specifies the arguments required to connect to a specific +// persistence mechanism. The arguments in the 'spec' string are separated by +// the '+' character. +// The first argument specifies the type of key/value store to be used; the +// meaning and format of the following arguments depend on this type. +// +// Key/Value Store types defined: +// * 'redis': Use a Redis server for persistance; the specification is +// "redis+addr+[passwd]+db". 'db' must be an integer value. +// * 'mysql': MySQL-compatible database (see 'database.go' for details) +// * 'sqlite3': SQLite3-compatible database (see 'database.go' for details) +func OpenKVStore(spec string) (KeyValueStore, error) { + // check specification string + specs := strings.Split(spec, "+") + if len(specs) < 2 { + return nil, ErrKVSInvalidSpec + } + switch specs[0] { + case "redis": + //-------------------------------------------------------------- + // NoSQL-based persistance + //-------------------------------------------------------------- + if len(specs) < 4 { + return nil, ErrKVSInvalidSpec + } + db, err := strconv.Atoi(specs[3]) + if err != nil { + return nil, ErrKVSInvalidSpec + } + kvs := new(KvsRedis) + kvs.db = db + kvs.client = redis.NewClient(&redis.Options{ + Addr: specs[1], + Password: specs[2], + DB: db, + }) + if kvs.client == nil { + err = ErrKVSNotAvailable + } + return kvs, err + + case "sqlite3", "mysql": + //-------------------------------------------------------------- + // SQL-based persistance + //-------------------------------------------------------------- + kvs := new(KvsSql) + var err error + + // connect to SQL database + kvs.db, err = ConnectSqlDatabase(spec) + if err != nil { + return nil, err + } + // get number of key/value pairs (as a check for existing table) + row := kvs.db.QueryRow("select count(*) from store") + var num int + if row.Scan(&num) != nil { + return nil, ErrKVSNotAvailable + } + return kvs, nil + } + return nil, ErrKVSInvalidSpec +} + +//====================================================================== +// NoSQL-based key-value-stores +//====================================================================== + +// Redis-based key/value store +type KvsRedis struct { + client *redis.Client // client connection + db int // index to database +} + +// Put a key/value pair into the store +func (kvs *KvsRedis) Put(key string, value string) error { + return kvs.client.Set(context.TODO(), key, value, 0).Err() +} + +// Get a value for a given key from store +func (kvs *KvsRedis) Get(key string) (value string, err error) { + return kvs.client.Get(context.TODO(), key).Result() +} + +// Get a list of all keys in store +func (kvs *KvsRedis) List() (keys []string, err error) { + var ( + crs uint64 + segm []string + ctx = context.TODO() + ) + for { + segm, crs, err = kvs.client.Scan(ctx, crs, "*", 10).Result() + if err != nil { + return nil, err + } + if crs == 0 { + break + } + keys = append(keys, segm...) + } + return +} + +//====================================================================== +// SQL-based key-value-store +//====================================================================== + +// SQL-based key/value store +type KvsSql struct { + db *sql.DB +} + +// Put a key/value pair into the store +func (kvs *KvsSql) Put(key string, value string) error { + _, err := kvs.db.Exec("insert into store(key,value) values(?,?)", key, value) + return err +} + +// Get a value for a given key from store +func (kvs *KvsSql) Get(key string) (value string, err error) { + row := kvs.db.QueryRow("select value from store where key=?", key) + err = row.Scan(&value) + return +} + +// Get a list of all keys in store +func (kvs *KvsSql) List() (keys []string, err error) { + var ( + rows *sql.Rows + key string + ) + rows, err = kvs.db.Query("select key from store") + if err == nil { + for rows.Next() { + if err = rows.Scan(&key); err != nil { + break + } + keys = append(keys, key) + } + } + return +} diff --git a/src/gnunet/util/time.go b/src/gnunet/util/time.go @@ -69,6 +69,18 @@ func (t AbsoluteTime) Add(d time.Duration) AbsoluteTime { } } +// Diff returns the relative time between two absolute times; +// the ordering of the absolute times doesn't matter. +func (t AbsoluteTime) Diff(t2 AbsoluteTime) RelativeTime { + var d uint64 + if t.Compare(t2) == 1 { + d = t.Val - t2.Val + } else { + d = t2.Val - t.Val + } + return RelativeTime{d} +} + // Expired returns true if the timestamp is in the past. func (t AbsoluteTime) Expired() bool { // check for "never"