mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-17 01:13:45 +00:00
Apply status-patches
This commit is contained in:
parent
4bb3c89d44
commit
805544030a
28 changed files with 1783 additions and 49 deletions
|
|
@ -1,3 +1,8 @@
|
||||||
|
# Go Ethereum (Status fork)
|
||||||
|
|
||||||
|
This is a forked version of the official `go-ethereum` repository. For detailed information on patches applied, see [https://github.com/status-im/status-go/geth-patches/](https://github.com/status-im/status-go/geth-patches/).
|
||||||
|
|
||||||
|
# Original README
|
||||||
## Go Ethereum
|
## Go Ethereum
|
||||||
|
|
||||||
Official golang implementation of the Ethereum protocol.
|
Official golang implementation of the Ethereum protocol.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -46,6 +47,10 @@ type Key struct {
|
||||||
// we only store privkey as pubkey/address can be derived from it
|
// we only store privkey as pubkey/address can be derived from it
|
||||||
// privkey in this struct is always in plaintext
|
// privkey in this struct is always in plaintext
|
||||||
PrivateKey *ecdsa.PrivateKey
|
PrivateKey *ecdsa.PrivateKey
|
||||||
|
// extended key is the root node for new hardened children i.e. sub-accounts
|
||||||
|
ExtendedKey *extkeys.ExtendedKey
|
||||||
|
// next index to be used for sub-account child derivation
|
||||||
|
SubAccountIndex uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyStore interface {
|
type keyStore interface {
|
||||||
|
|
@ -65,10 +70,12 @@ type plainKeyJSON struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type encryptedKeyJSONV3 struct {
|
type encryptedKeyJSONV3 struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Crypto cryptoJSON `json:"crypto"`
|
Crypto cryptoJSON `json:"crypto"`
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
|
ExtendedKey cryptoJSON `json:"extendedkey"`
|
||||||
|
SubAccountIndex uint32 `json:"subaccountindex"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type encryptedKeyJSONV1 struct {
|
type encryptedKeyJSONV1 struct {
|
||||||
|
|
@ -137,6 +144,40 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newKeyFromExtendedKey(extKey *extkeys.ExtendedKey) (*Key, error) {
|
||||||
|
var (
|
||||||
|
extChild1, extChild2 *extkeys.ExtendedKey
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if extKey.Depth == 0 { // we are dealing with master key
|
||||||
|
// CKD#1 - main account
|
||||||
|
extChild1, err = extKey.BIP44Child(extkeys.CoinTypeETH, 0)
|
||||||
|
if err != nil {
|
||||||
|
return &Key{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CKD#2 - sub-accounts root
|
||||||
|
extChild2, err = extKey.BIP44Child(extkeys.CoinTypeETH, 1)
|
||||||
|
if err != nil {
|
||||||
|
return &Key{}, err
|
||||||
|
}
|
||||||
|
} else { // we are dealing with non-master key, so it is safe to persist and extend from it
|
||||||
|
extChild1 = extKey
|
||||||
|
extChild2 = extKey
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyECDSA := extChild1.ToECDSA()
|
||||||
|
id := uuid.NewRandom()
|
||||||
|
key := &Key{
|
||||||
|
Id: id,
|
||||||
|
Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
|
||||||
|
PrivateKey: privateKeyECDSA,
|
||||||
|
ExtendedKey: extChild2,
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewKeyForDirectICAP generates a key whose address fits into < 155 bits so it can fit
|
// NewKeyForDirectICAP generates a key whose address fits into < 155 bits so it can fit
|
||||||
// into the Direct ICAP spec. for simplicity and easier compatibility with other libs, we
|
// into the Direct ICAP spec. for simplicity and easier compatibility with other libs, we
|
||||||
// retry until the first byte is 0.
|
// retry until the first byte is 0.
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -228,6 +229,11 @@ func (ks *KeyStore) Accounts() []accounts.Account {
|
||||||
return ks.cache.accounts()
|
return ks.cache.accounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AccountDecryptedKey returns decrypted key for account (provided that password is correct).
|
||||||
|
func (ks *KeyStore) AccountDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) {
|
||||||
|
return ks.getDecryptedKey(a, auth)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete deletes the key matched by account if the passphrase is correct.
|
// Delete deletes the key matched by account if the passphrase is correct.
|
||||||
// If the account contains no filename, the address must match a unique key.
|
// If the account contains no filename, the address must match a unique key.
|
||||||
func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
|
func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
|
||||||
|
|
@ -453,6 +459,34 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco
|
||||||
return ks.importKey(key, passphrase)
|
return ks.importKey(key, passphrase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportExtendedKey stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts)
|
||||||
|
// If key file is not found, it is created. Key is encrypted with the given passphrase.
|
||||||
|
func (ks *KeyStore) ImportExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
||||||
|
key, err := newKeyFromExtendedKey(extKey)
|
||||||
|
if err != nil {
|
||||||
|
zeroKey(key.PrivateKey)
|
||||||
|
return accounts.Account{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if account is already imported, return cached version
|
||||||
|
if ks.cache.hasAddress(key.Address) {
|
||||||
|
a := accounts.Account{
|
||||||
|
Address: key.Address,
|
||||||
|
}
|
||||||
|
ks.cache.maybeReload()
|
||||||
|
ks.cache.mu.Lock()
|
||||||
|
a, err := ks.cache.find(a)
|
||||||
|
ks.cache.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
zeroKey(key.PrivateKey)
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ks.importKey(key, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
|
func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
|
||||||
a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}}
|
a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}}
|
||||||
if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil {
|
if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil {
|
||||||
|
|
@ -463,6 +497,15 @@ func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, er
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ks *KeyStore) IncSubAccountIndex(a accounts.Account, passphrase string) error {
|
||||||
|
a, key, err := ks.getDecryptedKey(a, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key.SubAccountIndex++
|
||||||
|
return ks.storage.StoreKey(a.URL.Path, key, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
// Update changes the passphrase of an existing account.
|
// Update changes the passphrase of an existing account.
|
||||||
func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error {
|
func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error {
|
||||||
a, key, err := ks.getDecryptedKey(a, passphrase)
|
a, key, err := ks.getDecryptedKey(a, passphrase)
|
||||||
|
|
@ -486,6 +529,9 @@ func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (account
|
||||||
|
|
||||||
// zeroKey zeroes a private key in memory.
|
// zeroKey zeroes a private key in memory.
|
||||||
func zeroKey(k *ecdsa.PrivateKey) {
|
func zeroKey(k *ecdsa.PrivateKey) {
|
||||||
|
if k == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
b := k.D.Bits()
|
b := k.D.Bits()
|
||||||
for i := range b {
|
for i := range b {
|
||||||
b[i] = 0
|
b[i] = 0
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/crypto/randentropy"
|
"github.com/ethereum/go-ethereum/crypto/randentropy"
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
"golang.org/x/crypto/pbkdf2"
|
"golang.org/x/crypto/pbkdf2"
|
||||||
"golang.org/x/crypto/scrypt"
|
"golang.org/x/crypto/scrypt"
|
||||||
)
|
)
|
||||||
|
|
@ -151,15 +152,62 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) {
|
||||||
KDFParams: scryptParamsJSON,
|
KDFParams: scryptParamsJSON,
|
||||||
MAC: hex.EncodeToString(mac),
|
MAC: hex.EncodeToString(mac),
|
||||||
}
|
}
|
||||||
|
encryptedExtendedKey, err := EncryptExtendedKey(key.ExtendedKey, auth, scryptN, scryptP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
encryptedKeyJSONV3 := encryptedKeyJSONV3{
|
encryptedKeyJSONV3 := encryptedKeyJSONV3{
|
||||||
hex.EncodeToString(key.Address[:]),
|
hex.EncodeToString(key.Address[:]),
|
||||||
cryptoStruct,
|
cryptoStruct,
|
||||||
key.Id.String(),
|
key.Id.String(),
|
||||||
version,
|
version,
|
||||||
|
encryptedExtendedKey,
|
||||||
|
key.SubAccountIndex,
|
||||||
}
|
}
|
||||||
return json.Marshal(encryptedKeyJSONV3)
|
return json.Marshal(encryptedKeyJSONV3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EncryptExtendedKey(extKey *extkeys.ExtendedKey, auth string, scryptN, scryptP int) (cryptoJSON, error) {
|
||||||
|
if extKey == nil {
|
||||||
|
return cryptoJSON{}, nil
|
||||||
|
}
|
||||||
|
authArray := []byte(auth)
|
||||||
|
salt := randentropy.GetEntropyCSPRNG(32)
|
||||||
|
derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen)
|
||||||
|
if err != nil {
|
||||||
|
return cryptoJSON{}, err
|
||||||
|
}
|
||||||
|
encryptKey := derivedKey[:16]
|
||||||
|
keyBytes := []byte(extKey.String())
|
||||||
|
|
||||||
|
iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16
|
||||||
|
cipherText, err := aesCTRXOR(encryptKey, keyBytes, iv)
|
||||||
|
if err != nil {
|
||||||
|
return cryptoJSON{}, err
|
||||||
|
}
|
||||||
|
mac := crypto.Keccak256(derivedKey[16:32], cipherText)
|
||||||
|
|
||||||
|
scryptParamsJSON := make(map[string]interface{}, 5)
|
||||||
|
scryptParamsJSON["n"] = scryptN
|
||||||
|
scryptParamsJSON["r"] = scryptR
|
||||||
|
scryptParamsJSON["p"] = scryptP
|
||||||
|
scryptParamsJSON["dklen"] = scryptDKLen
|
||||||
|
scryptParamsJSON["salt"] = hex.EncodeToString(salt)
|
||||||
|
|
||||||
|
cipherParamsJSON := cipherparamsJSON{
|
||||||
|
IV: hex.EncodeToString(iv),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cryptoJSON{
|
||||||
|
Cipher: "aes-128-ctr",
|
||||||
|
CipherText: hex.EncodeToString(cipherText),
|
||||||
|
CipherParams: cipherParamsJSON,
|
||||||
|
KDF: "scrypt",
|
||||||
|
KDFParams: scryptParamsJSON,
|
||||||
|
MAC: hex.EncodeToString(mac),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DecryptKey decrypts a key from a json blob, returning the private key itself.
|
// DecryptKey decrypts a key from a json blob, returning the private key itself.
|
||||||
func DecryptKey(keyjson []byte, auth string) (*Key, error) {
|
func DecryptKey(keyjson []byte, auth string) (*Key, error) {
|
||||||
// Parse the json into a simple map to fetch the key version
|
// Parse the json into a simple map to fetch the key version
|
||||||
|
|
@ -171,20 +219,43 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) {
|
||||||
var (
|
var (
|
||||||
keyBytes, keyId []byte
|
keyBytes, keyId []byte
|
||||||
err error
|
err error
|
||||||
|
extKeyBytes []byte
|
||||||
|
extKey *extkeys.ExtendedKey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
subAccountIndex, ok := m["subaccountindex"].(float64)
|
||||||
|
if !ok {
|
||||||
|
subAccountIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
if version, ok := m["version"].(string); ok && version == "1" {
|
if version, ok := m["version"].(string); ok && version == "1" {
|
||||||
k := new(encryptedKeyJSONV1)
|
k := new(encryptedKeyJSONV1)
|
||||||
if err := json.Unmarshal(keyjson, k); err != nil {
|
if err := json.Unmarshal(keyjson, k); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
keyBytes, keyId, err = decryptKeyV1(k, auth)
|
keyBytes, keyId, err = decryptKeyV1(k, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
extKey, err = extkeys.NewKeyFromString(extkeys.EmptyExtendedKeyString)
|
||||||
} else {
|
} else {
|
||||||
k := new(encryptedKeyJSONV3)
|
k := new(encryptedKeyJSONV3)
|
||||||
if err := json.Unmarshal(keyjson, k); err != nil {
|
if err := json.Unmarshal(keyjson, k); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
keyBytes, keyId, err = decryptKeyV3(k, auth)
|
keyBytes, keyId, err = decryptKeyV3(k, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
extKeyBytes, err = decryptExtendedKey(k, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
extKey, err = extkeys.NewKeyFromString(string(extKeyBytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle any decryption errors and return the key
|
// Handle any decryption errors and return the key
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -192,9 +263,11 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) {
|
||||||
key := crypto.ToECDSAUnsafe(keyBytes)
|
key := crypto.ToECDSAUnsafe(keyBytes)
|
||||||
|
|
||||||
return &Key{
|
return &Key{
|
||||||
Id: uuid.UUID(keyId),
|
Id: uuid.UUID(keyId),
|
||||||
Address: crypto.PubkeyToAddress(key.PublicKey),
|
Address: crypto.PubkeyToAddress(key.PublicKey),
|
||||||
PrivateKey: key,
|
PrivateKey: key,
|
||||||
|
ExtendedKey: extKey,
|
||||||
|
SubAccountIndex: uint32(subAccountIndex),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,6 +347,51 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt
|
||||||
return plainText, keyId, err
|
return plainText, keyId, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decryptExtendedKey(keyProtected *encryptedKeyJSONV3, auth string) (plainText []byte, err error) {
|
||||||
|
if len(keyProtected.ExtendedKey.CipherText) == 0 {
|
||||||
|
return []byte(extkeys.EmptyExtendedKeyString), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyProtected.Version != version {
|
||||||
|
return nil, fmt.Errorf("Version not supported: %v", keyProtected.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyProtected.ExtendedKey.Cipher != "aes-128-ctr" {
|
||||||
|
return nil, fmt.Errorf("Cipher not supported: %v", keyProtected.ExtendedKey.Cipher)
|
||||||
|
}
|
||||||
|
|
||||||
|
mac, err := hex.DecodeString(keyProtected.ExtendedKey.MAC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
iv, err := hex.DecodeString(keyProtected.ExtendedKey.CipherParams.IV)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherText, err := hex.DecodeString(keyProtected.ExtendedKey.CipherText)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
derivedKey, err := getKDFKey(keyProtected.ExtendedKey, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText)
|
||||||
|
if !bytes.Equal(calculatedMAC, mac) {
|
||||||
|
return nil, ErrDecrypt
|
||||||
|
}
|
||||||
|
|
||||||
|
plainText, err = aesCTRXOR(derivedKey[:16], cipherText, iv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return plainText, err
|
||||||
|
}
|
||||||
|
|
||||||
func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) {
|
func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) {
|
||||||
authArray := []byte(auth)
|
authArray := []byte(auth)
|
||||||
salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string))
|
salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string))
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ var (
|
||||||
argEnode = flag.String("boot", "", "bootstrap node you want to connect to (e.g. enode://e454......08d50@52.176.211.200:16428)")
|
argEnode = flag.String("boot", "", "bootstrap node you want to connect to (e.g. enode://e454......08d50@52.176.211.200:16428)")
|
||||||
argTopic = flag.String("topic", "", "topic in hexadecimal format (e.g. 70a4beef)")
|
argTopic = flag.String("topic", "", "topic in hexadecimal format (e.g. 70a4beef)")
|
||||||
argSaveDir = flag.String("savedir", "", "directory where incoming messages will be saved as files")
|
argSaveDir = flag.String("savedir", "", "directory where incoming messages will be saved as files")
|
||||||
|
argSymPass = flag.String("sympass", "", "SymKey password")
|
||||||
|
argMsPass = flag.String("mspass", "", "Mailserver password")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -146,6 +148,13 @@ func processArgs() {
|
||||||
} else if *fileExMode {
|
} else if *fileExMode {
|
||||||
utils.Fatalf("Parameter 'savedir' is mandatory for file exchange mode")
|
utils.Fatalf("Parameter 'savedir' is mandatory for file exchange mode")
|
||||||
}
|
}
|
||||||
|
if len(*argSymPass) > 0 {
|
||||||
|
symPass = *argSymPass
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*argMsPass) > 0 {
|
||||||
|
msPassword = *argMsPass
|
||||||
|
}
|
||||||
|
|
||||||
if *echoMode {
|
if *echoMode {
|
||||||
echo()
|
echo()
|
||||||
|
|
@ -415,10 +424,24 @@ func run() {
|
||||||
} else if *fileExMode {
|
} else if *fileExMode {
|
||||||
sendFilesLoop()
|
sendFilesLoop()
|
||||||
} else {
|
} else {
|
||||||
sendLoop()
|
pingLoop() // instead of sendLoop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pingLoop() {
|
||||||
|
ticker := time.NewTicker(time.Second * 120)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
fmt.Println("I am alive: ", time.Now())
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func sendLoop() {
|
func sendLoop() {
|
||||||
for {
|
for {
|
||||||
s := scanLine("")
|
s := scanLine("")
|
||||||
|
|
|
||||||
65
common/message/message.go
Normal file
65
common/message/message.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package message
|
||||||
|
|
||||||
|
// Direction defines a int type to indicate a message as either incoming or outgoing.
|
||||||
|
type Direction int
|
||||||
|
|
||||||
|
// consts of all message direction values.
|
||||||
|
const (
|
||||||
|
IncomingMessage Direction = iota + 1
|
||||||
|
OutgoingMessage
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the representation of giving direction.
|
||||||
|
func (d Direction) String() string {
|
||||||
|
switch d {
|
||||||
|
case IncomingMessage:
|
||||||
|
return "IncomingMessage"
|
||||||
|
case OutgoingMessage:
|
||||||
|
return "OutgoingMessage"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "MessageDirectionUnknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status defines a int type to indicate different status value of a
|
||||||
|
// message state.
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
// consts of all message delivery status.
|
||||||
|
const (
|
||||||
|
PendingStatus Status = iota + 1
|
||||||
|
QueuedStatus
|
||||||
|
CachedStatus
|
||||||
|
SentStatus
|
||||||
|
ExpiredStatus
|
||||||
|
ProcessingStatus
|
||||||
|
ResentStatus
|
||||||
|
RejectedStatus
|
||||||
|
DeliveredStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the representation of giving state.
|
||||||
|
func (s Status) String() string {
|
||||||
|
switch s {
|
||||||
|
case PendingStatus:
|
||||||
|
return "Pending"
|
||||||
|
case QueuedStatus:
|
||||||
|
return "Queued"
|
||||||
|
case CachedStatus:
|
||||||
|
return "Cached"
|
||||||
|
case SentStatus:
|
||||||
|
return "Sent"
|
||||||
|
case ProcessingStatus:
|
||||||
|
return "Processing"
|
||||||
|
case ExpiredStatus:
|
||||||
|
return "ExpiredTTL"
|
||||||
|
case ResentStatus:
|
||||||
|
return "Resent"
|
||||||
|
case RejectedStatus:
|
||||||
|
return "Rejected"
|
||||||
|
case DeliveredStatus:
|
||||||
|
return "Delivered"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
20
containers/docker/status-alpine/geth/Dockerfile
Normal file
20
containers/docker/status-alpine/geth/Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
FROM alpine:3.5
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --update go git make gcc musl-dev linux-headers ca-certificates && \
|
||||||
|
|
||||||
|
# clone status-go
|
||||||
|
mkdir -p /usr/lib/go/src/github.com/status-im && \
|
||||||
|
git clone --depth 1 --branch 0.9.7 https://github.com/status-im/status-go.git /usr/lib/go/src/github.com/status-im/status-go && \
|
||||||
|
|
||||||
|
# clone go-ethereum (and install everything)
|
||||||
|
git clone --depth 1 --branch status/1.6.1-stable https://github.com/status-im/go-ethereum && \
|
||||||
|
(cd go-ethereum && make geth) && \
|
||||||
|
cp go-ethereum/build/bin/geth /geth && \
|
||||||
|
apk del go git make gcc musl-dev linux-headers && \
|
||||||
|
rm -rf /go-ethereum && rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
EXPOSE 8545
|
||||||
|
EXPOSE 30303
|
||||||
|
|
||||||
|
ENTRYPOINT ["/geth"]
|
||||||
19
containers/docker/status-alpine/swarm/Dockerfile
Normal file
19
containers/docker/status-alpine/swarm/Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
FROM alpine:3.5
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --update go git make gcc musl-dev linux-headers ca-certificates && \
|
||||||
|
|
||||||
|
# clone status-go
|
||||||
|
mkdir -p /usr/lib/go/src/github.com/status-im && \
|
||||||
|
git clone --depth 1 --branch develop https://github.com/status-im/status-go.git /usr/lib/go/src/github.com/status-im/status-go && \
|
||||||
|
|
||||||
|
# clone go-ethereum (and install everything)
|
||||||
|
git clone --depth 1 --branch status/1.6.1-stable https://github.com/status-im/go-ethereum && \
|
||||||
|
(cd go-ethereum && build/env.sh go run build/ci.go install ./cmd/swarm) && \
|
||||||
|
cp go-ethereum/build/bin/swarm /swarm && \
|
||||||
|
apk del go git make gcc musl-dev linux-headers && \
|
||||||
|
rm -rf /go-ethereum && rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
EXPOSE 30399
|
||||||
|
|
||||||
|
ENTRYPOINT ["/swarm"]
|
||||||
19
containers/docker/status-alpine/wnode/Dockerfile
Normal file
19
containers/docker/status-alpine/wnode/Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
FROM alpine:3.5
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --update go git make gcc musl-dev linux-headers ca-certificates && \
|
||||||
|
|
||||||
|
# clone status-go
|
||||||
|
mkdir -p /usr/lib/go/src/github.com/status-im && \
|
||||||
|
git clone --depth 1 --branch develop https://github.com/status-im/status-go.git /usr/lib/go/src/github.com/status-im/status-go && \
|
||||||
|
|
||||||
|
# clone go-ethereum (and install everything)
|
||||||
|
git clone --depth 1 --branch status/1.6.1-stable https://github.com/status-im/go-ethereum && \
|
||||||
|
(cd go-ethereum && build/env.sh go run build/ci.go install ./cmd/wnode) && \
|
||||||
|
cp go-ethereum/build/bin/wnode /wnode && \
|
||||||
|
apk del go git make gcc musl-dev linux-headers && \
|
||||||
|
rm -rf /go-ethereum && rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
EXPOSE 30379
|
||||||
|
|
||||||
|
ENTRYPOINT ["/wnode"]
|
||||||
|
|
@ -32,14 +32,20 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/eth/gasprice"
|
"github.com/ethereum/go-ethereum/eth/gasprice"
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EthApiBackend implements ethapi.Backend for full nodes
|
// EthApiBackend implements ethapi.Backend for full nodes
|
||||||
type EthApiBackend struct {
|
type EthApiBackend struct {
|
||||||
eth *Ethereum
|
eth *Ethereum
|
||||||
gpo *gasprice.Oracle
|
gpo *gasprice.Oracle
|
||||||
|
statusBackend *ethapi.StatusBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *EthApiBackend) GetStatusBackend() *ethapi.StatusBackend {
|
||||||
|
return b.statusBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *EthApiBackend) ChainConfig() *params.ChainConfig {
|
func (b *EthApiBackend) ChainConfig() *params.ChainConfig {
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
|
||||||
eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)
|
eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)
|
||||||
eth.miner.SetExtra(makeExtraData(config.ExtraData))
|
eth.miner.SetExtra(makeExtraData(config.ExtraData))
|
||||||
|
|
||||||
eth.ApiBackend = &EthApiBackend{eth, nil}
|
eth.ApiBackend = &EthApiBackend{eth, nil, nil}
|
||||||
gpoParams := config.GPO
|
gpoParams := config.GPO
|
||||||
if gpoParams.Default == nil {
|
if gpoParams.Default == nil {
|
||||||
gpoParams.Default = config.GasPrice
|
gpoParams.Default = config.GasPrice
|
||||||
|
|
|
||||||
|
|
@ -178,15 +178,24 @@ func (s *PublicTxPoolAPI) Inspect() map[string]map[string]map[string]string {
|
||||||
// It offers only methods that can retrieve accounts.
|
// It offers only methods that can retrieve accounts.
|
||||||
type PublicAccountAPI struct {
|
type PublicAccountAPI struct {
|
||||||
am *accounts.Manager
|
am *accounts.Manager
|
||||||
|
b Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPublicAccountAPI creates a new PublicAccountAPI.
|
// NewPublicAccountAPI creates a new PublicAccountAPI.
|
||||||
func NewPublicAccountAPI(am *accounts.Manager) *PublicAccountAPI {
|
func NewPublicAccountAPI(b Backend) *PublicAccountAPI {
|
||||||
return &PublicAccountAPI{am: am}
|
return &PublicAccountAPI{
|
||||||
|
am: b.AccountManager(),
|
||||||
|
b: b,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accounts returns the collection of accounts this node manages
|
// Accounts returns the collection of accounts this node manages
|
||||||
func (s *PublicAccountAPI) Accounts() []common.Address {
|
func (s *PublicAccountAPI) Accounts() []common.Address {
|
||||||
|
backend := s.b.GetStatusBackend()
|
||||||
|
if backend != nil {
|
||||||
|
return backend.am.Accounts()
|
||||||
|
}
|
||||||
|
|
||||||
addresses := make([]common.Address, 0) // return [] instead of nil if empty
|
addresses := make([]common.Address, 0) // return [] instead of nil if empty
|
||||||
for _, wallet := range s.am.Wallets() {
|
for _, wallet := range s.am.Wallets() {
|
||||||
for _, account := range wallet.Accounts() {
|
for _, account := range wallet.Accounts() {
|
||||||
|
|
@ -216,6 +225,11 @@ func NewPrivateAccountAPI(b Backend, nonceLock *AddrLocker) *PrivateAccountAPI {
|
||||||
|
|
||||||
// ListAccounts will return a list of addresses for accounts this node manages.
|
// ListAccounts will return a list of addresses for accounts this node manages.
|
||||||
func (s *PrivateAccountAPI) ListAccounts() []common.Address {
|
func (s *PrivateAccountAPI) ListAccounts() []common.Address {
|
||||||
|
backend := s.b.GetStatusBackend()
|
||||||
|
if backend != nil {
|
||||||
|
return backend.am.Accounts()
|
||||||
|
}
|
||||||
|
|
||||||
addresses := make([]common.Address, 0) // return [] instead of nil if empty
|
addresses := make([]common.Address, 0) // return [] instead of nil if empty
|
||||||
for _, wallet := range s.am.Wallets() {
|
for _, wallet := range s.am.Wallets() {
|
||||||
for _, account := range wallet.Accounts() {
|
for _, account := range wallet.Accounts() {
|
||||||
|
|
@ -1122,10 +1136,46 @@ func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c
|
||||||
return tx.Hash(), nil
|
return tx.Hash(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTransaction creates a transaction for the given argument, sign it and submit it to the
|
// SendTransactionWithPassphrase creates a transaction by unpacking queued transaction, signs it and submits to the
|
||||||
|
// transaction pool.
|
||||||
|
// @Status
|
||||||
|
func (s *PublicTransactionPoolAPI) SendTransactionWithPassphrase(ctx context.Context, args SendTxArgs, passphrase string) (common.Hash, error) {
|
||||||
|
// Look up the wallet containing the requested signer
|
||||||
|
account := accounts.Account{Address: args.From}
|
||||||
|
|
||||||
|
wallet, err := s.b.AccountManager().Find(account)
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Nonce == nil {
|
||||||
|
// Hold the addresse's mutex around signing to prevent concurrent assignment of
|
||||||
|
// the same nonce to multiple accounts.
|
||||||
|
s.nonceLock.LockAddr(args.From)
|
||||||
|
defer s.nonceLock.UnlockAddr(args.From)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set some sanity defaults and terminate on failure
|
||||||
|
if err := args.setDefaults(ctx, s.b); err != nil {
|
||||||
|
return common.Hash{}, err
|
||||||
|
}
|
||||||
|
// Assemble the transaction and sign with the wallet
|
||||||
|
tx := args.toTransaction()
|
||||||
|
|
||||||
|
var chainID *big.Int
|
||||||
|
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
|
||||||
|
chainID = config.ChainId
|
||||||
|
}
|
||||||
|
signed, err := wallet.SignTxWithPassphrase(account, passphrase, tx, chainID)
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}, err
|
||||||
|
}
|
||||||
|
return submitTransaction(ctx, s.b, signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendTransaction creates a transaction by unpacking queued transaction, signs it and submits to the
|
||||||
// transaction pool.
|
// transaction pool.
|
||||||
func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) {
|
func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) {
|
||||||
|
|
||||||
// Look up the wallet containing the requested signer
|
// Look up the wallet containing the requested signer
|
||||||
account := accounts.Account{Address: args.From}
|
account := accounts.Account{Address: args.From}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ type Backend interface {
|
||||||
|
|
||||||
ChainConfig() *params.ChainConfig
|
ChainConfig() *params.ChainConfig
|
||||||
CurrentBlock() *types.Block
|
CurrentBlock() *types.Block
|
||||||
|
|
||||||
|
GetStatusBackend() *StatusBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAPIs(apiBackend Backend) []rpc.API {
|
func GetAPIs(apiBackend Backend) []rpc.API {
|
||||||
|
|
@ -105,7 +107,7 @@ func GetAPIs(apiBackend Backend) []rpc.API {
|
||||||
}, {
|
}, {
|
||||||
Namespace: "eth",
|
Namespace: "eth",
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
Service: NewPublicAccountAPI(apiBackend.AccountManager()),
|
Service: NewPublicAccountAPI(apiBackend),
|
||||||
Public: true,
|
Public: true,
|
||||||
}, {
|
}, {
|
||||||
Namespace: "personal",
|
Namespace: "personal",
|
||||||
|
|
|
||||||
88
internal/ethapi/status_backend.go
Normal file
88
internal/ethapi/status_backend.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package ethapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/les/status"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusBackend exposes Ethereum internals to support custom semantics in status-go bindings
|
||||||
|
type StatusBackend struct {
|
||||||
|
eapi *PublicEthereumAPI // Wrapper around the Ethereum object to access metadata
|
||||||
|
bcapi *PublicBlockChainAPI // Wrapper around the blockchain to access chain data
|
||||||
|
txapi *PublicTransactionPoolAPI // Wrapper around the transaction pool to access transaction data
|
||||||
|
|
||||||
|
am *status.AccountManager
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrStatusBackendNotInited = errors.New("StatusIM backend is not properly inited")
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewStatusBackend creates a new backend using an existing Ethereum object.
|
||||||
|
func NewStatusBackend(apiBackend Backend) *StatusBackend {
|
||||||
|
log.Info("StatusIM: backend service inited")
|
||||||
|
return &StatusBackend{
|
||||||
|
eapi: NewPublicEthereumAPI(apiBackend),
|
||||||
|
bcapi: NewPublicBlockChainAPI(apiBackend),
|
||||||
|
txapi: NewPublicTransactionPoolAPI(apiBackend, new(AddrLocker)),
|
||||||
|
am: status.NewAccountManager(apiBackend.AccountManager()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAccountsFilterHandler sets a callback that is triggered when account list is requested
|
||||||
|
func (b *StatusBackend) SetAccountsFilterHandler(fn status.AccountsFilterHandler) {
|
||||||
|
b.am.SetAccountsFilterHandler(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountManager returns reference to account manager
|
||||||
|
func (b *StatusBackend) AccountManager() *status.AccountManager {
|
||||||
|
return b.am
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendTransaction wraps call to PublicTransactionPoolAPI.SendTransactionWithPassphrase
|
||||||
|
func (b *StatusBackend) SendTransaction(ctx context.Context, args status.SendTxArgs, passphrase string) (common.Hash, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
if estimatedGas, err := b.EstimateGas(ctx, args); err == nil {
|
||||||
|
if estimatedGas.ToInt().Cmp(big.NewInt(defaultGas)) == 1 { // gas > defaultGas
|
||||||
|
args.Gas = estimatedGas
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.txapi.SendTransactionWithPassphrase(ctx, SendTxArgs(args), passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateGas uses underlying blockchain API to obtain gas for a given tx arguments
|
||||||
|
func (b *StatusBackend) EstimateGas(ctx context.Context, args status.SendTxArgs) (*hexutil.Big, error) {
|
||||||
|
if args.Gas != nil {
|
||||||
|
return args.Gas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var gasPrice hexutil.Big
|
||||||
|
if args.GasPrice != nil {
|
||||||
|
gasPrice = *args.GasPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
var value hexutil.Big
|
||||||
|
if args.Value != nil {
|
||||||
|
value = *args.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
callArgs := CallArgs{
|
||||||
|
From: args.From,
|
||||||
|
To: args.To,
|
||||||
|
GasPrice: gasPrice,
|
||||||
|
Value: value,
|
||||||
|
Data: args.Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.bcapi.EstimateGas(ctx, callArgs)
|
||||||
|
}
|
||||||
|
|
@ -32,14 +32,20 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/eth/gasprice"
|
"github.com/ethereum/go-ethereum/eth/gasprice"
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||||
"github.com/ethereum/go-ethereum/light"
|
"github.com/ethereum/go-ethereum/light"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LesApiBackend struct {
|
type LesApiBackend struct {
|
||||||
eth *LightEthereum
|
eth *LightEthereum
|
||||||
gpo *gasprice.Oracle
|
gpo *gasprice.Oracle
|
||||||
|
statusBackend *ethapi.StatusBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LesApiBackend) GetStatusBackend() *ethapi.StatusBackend {
|
||||||
|
return b.statusBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LesApiBackend) ChainConfig() *params.ChainConfig {
|
func (b *LesApiBackend) ChainConfig() *params.ChainConfig {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ type LightEthereum struct {
|
||||||
netRPCService *ethapi.PublicNetAPI
|
netRPCService *ethapi.PublicNetAPI
|
||||||
|
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
StatusBackend *ethapi.StatusBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
|
func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
|
||||||
|
|
@ -126,12 +128,17 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
|
||||||
if leth.protocolManager, err = NewProtocolManager(leth.chainConfig, true, ClientProtocolVersions, config.NetworkId, leth.eventMux, leth.engine, leth.peers, leth.blockchain, nil, chainDb, leth.odr, leth.relay, quitSync, &leth.wg); err != nil {
|
if leth.protocolManager, err = NewProtocolManager(leth.chainConfig, true, ClientProtocolVersions, config.NetworkId, leth.eventMux, leth.engine, leth.peers, leth.blockchain, nil, chainDb, leth.odr, leth.relay, quitSync, &leth.wg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
leth.ApiBackend = &LesApiBackend{leth, nil}
|
leth.ApiBackend = &LesApiBackend{leth, nil, nil}
|
||||||
gpoParams := config.GPO
|
gpoParams := config.GPO
|
||||||
if gpoParams.Default == nil {
|
if gpoParams.Default == nil {
|
||||||
gpoParams.Default = config.GasPrice
|
gpoParams.Default = config.GasPrice
|
||||||
}
|
}
|
||||||
leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams)
|
leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams)
|
||||||
|
|
||||||
|
// inject status-im backend
|
||||||
|
leth.ApiBackend.statusBackend = ethapi.NewStatusBackend(leth.ApiBackend)
|
||||||
|
leth.StatusBackend = leth.ApiBackend.statusBackend // alias
|
||||||
|
|
||||||
return leth, nil
|
return leth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
45
les/status/accounts.go
Normal file
45
les/status/accounts.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountManager abstracts both internal account manager and extra filter status backend requires
|
||||||
|
type AccountManager struct {
|
||||||
|
am *accounts.Manager
|
||||||
|
accountsFilterHandler AccountsFilterHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAccountManager creates a new AccountManager
|
||||||
|
func NewAccountManager(am *accounts.Manager) *AccountManager {
|
||||||
|
return &AccountManager{
|
||||||
|
am: am,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountsFilterHandler function to filter out accounts list
|
||||||
|
type AccountsFilterHandler func([]common.Address) []common.Address
|
||||||
|
|
||||||
|
// Accounts returns accounts' addresses of currently logged in user.
|
||||||
|
// Since status supports HD keys, the following list is returned:
|
||||||
|
// [addressCDK#1, addressCKD#2->Child1, addressCKD#2->Child2, .. addressCKD#2->ChildN]
|
||||||
|
func (d *AccountManager) Accounts() []common.Address {
|
||||||
|
var addresses []common.Address
|
||||||
|
for _, wallet := range d.am.Wallets() {
|
||||||
|
for _, account := range wallet.Accounts() {
|
||||||
|
addresses = append(addresses, account.Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.accountsFilterHandler != nil {
|
||||||
|
return d.accountsFilterHandler(addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAccountsFilterHandler sets filtering function for accounts list
|
||||||
|
func (d *AccountManager) SetAccountsFilterHandler(fn AccountsFilterHandler) {
|
||||||
|
d.accountsFilterHandler = fn
|
||||||
|
}
|
||||||
17
les/status/types.go
Normal file
17
les/status/types.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendTxArgs represents the arguments to submit a new transaction into the transaction pool.
|
||||||
|
type SendTxArgs struct {
|
||||||
|
From common.Address `json:"from"`
|
||||||
|
To *common.Address `json:"to"`
|
||||||
|
Gas *hexutil.Big `json:"gas"`
|
||||||
|
GasPrice *hexutil.Big `json:"gasPrice"`
|
||||||
|
Value *hexutil.Big `json:"value"`
|
||||||
|
Data hexutil.Bytes `json:"data"`
|
||||||
|
Nonce *hexutil.Uint64 `json:"nonce"`
|
||||||
|
}
|
||||||
|
|
@ -66,12 +66,20 @@ var (
|
||||||
chtRoot: common.HexToHash("6f56dc61936752cc1f8c84b4addabdbe6a1c19693de3f21cb818362df2117f03"),
|
chtRoot: common.HexToHash("6f56dc61936752cc1f8c84b4addabdbe6a1c19693de3f21cb818362df2117f03"),
|
||||||
bloomTrieRoot: common.HexToHash("aca7d7c504d22737242effc3fdc604a762a0af9ced898036b5986c3a15220208"),
|
bloomTrieRoot: common.HexToHash("aca7d7c504d22737242effc3fdc604a762a0af9ced898036b5986c3a15220208"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusRopstenCheckpoint = trustedCheckpoint{
|
||||||
|
name: "Ropsten testnet",
|
||||||
|
sectionIdx: 67,
|
||||||
|
sectionHead: common.HexToHash("9832cf2ce760d4e3a7922fbfedeaa5dce67f1772e0f729f67c806bfafdedc370"),
|
||||||
|
chtRoot: common.HexToHash("60d43984a1d55e93f4296f4b48bf5af350476fe48679a73263bd57d8a324c9d4"),
|
||||||
|
bloomTrieRoot: common.HexToHash("fd81543dc619f6d1148e766b942c90296343c2cd0fd464946678f27f35feb59b"),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// trustedCheckpoints associates each known checkpoint with the genesis hash of the chain it belongs to
|
// trustedCheckpoints associates each known checkpoint with the genesis hash of the chain it belongs to
|
||||||
var trustedCheckpoints = map[common.Hash]trustedCheckpoint{
|
var trustedCheckpoints = map[common.Hash]trustedCheckpoint{
|
||||||
params.MainnetGenesisHash: mainnetCheckpoint,
|
params.MainnetGenesisHash: mainnetCheckpoint,
|
||||||
params.TestnetGenesisHash: ropstenCheckpoint,
|
params.TestnetGenesisHash: statusRopstenCheckpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
154
whisper/notifications/discovery.go
Normal file
154
whisper/notifications/discovery.go
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
topicDiscoverServer = "DISCOVER_NOTIFICATION_SERVER"
|
||||||
|
topicProposeServer = "PROPOSE_NOTIFICATION_SERVER"
|
||||||
|
topicServerAccepted = "ACCEPT_NOTIFICATION_SERVER"
|
||||||
|
topicAckClientSubscription = "ACK_NOTIFICATION_SERVER_SUBSCRIPTION"
|
||||||
|
)
|
||||||
|
|
||||||
|
// discoveryService abstract notification server discovery protocol
|
||||||
|
type discoveryService struct {
|
||||||
|
server *NotificationServer
|
||||||
|
|
||||||
|
discoverFilterID string
|
||||||
|
serverAcceptedFilterID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// messageProcessingFn is a callback used to process incoming client requests
|
||||||
|
type messageProcessingFn func(*whisper.ReceivedMessage) error
|
||||||
|
|
||||||
|
func NewDiscoveryService(notificationServer *NotificationServer) *discoveryService {
|
||||||
|
return &discoveryService{
|
||||||
|
server: notificationServer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start installs necessary filters to watch for incoming discovery requests,
|
||||||
|
// then in separate routine starts watcher loop
|
||||||
|
func (s *discoveryService) Start() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// notification server discovery requests
|
||||||
|
s.discoverFilterID, err = s.server.installKeyFilter(topicDiscoverServer, s.server.protocolKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
go s.server.requestProcessorLoop(s.discoverFilterID, topicDiscoverServer, s.processDiscoveryRequest)
|
||||||
|
|
||||||
|
// notification server accept/select requests
|
||||||
|
s.serverAcceptedFilterID, err = s.server.installKeyFilter(topicServerAccepted, s.server.protocolKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
go s.server.requestProcessorLoop(s.serverAcceptedFilterID, topicServerAccepted, s.processServerAcceptedRequest)
|
||||||
|
|
||||||
|
log.Info("notification server discovery service started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops all discovery processing loops
|
||||||
|
func (s *discoveryService) Stop() error {
|
||||||
|
s.server.whisper.Unsubscribe(s.discoverFilterID)
|
||||||
|
s.server.whisper.Unsubscribe(s.serverAcceptedFilterID)
|
||||||
|
|
||||||
|
log.Info("notification server discovery service stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processDiscoveryRequest processes incoming client requests of type:
|
||||||
|
// when client tries to discover suitable notification server
|
||||||
|
func (s *discoveryService) processDiscoveryRequest(msg *whisper.ReceivedMessage) error {
|
||||||
|
// offer this node as notification server
|
||||||
|
msgParams := whisper.MessageParams{
|
||||||
|
Src: s.server.protocolKey,
|
||||||
|
Dst: msg.Src,
|
||||||
|
Topic: MakeTopic([]byte(topicProposeServer)),
|
||||||
|
Payload: []byte(`{"server": "0x` + s.server.nodeID + `"}`),
|
||||||
|
TTL: uint32(s.server.config.TTL),
|
||||||
|
PoW: s.server.config.MinimumPoW,
|
||||||
|
WorkTime: 5,
|
||||||
|
}
|
||||||
|
response, err := whisper.NewSentMessage(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create proposal message: %v", err)
|
||||||
|
}
|
||||||
|
env, err := response.Wrap(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to wrap server proposal message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.server.whisper.Send(env); err != nil {
|
||||||
|
return fmt.Errorf("failed to send server proposal message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(fmt.Sprintf("server proposal sent (server: %v, dst: %v, topic: %x)",
|
||||||
|
s.server.nodeID, common.ToHex(crypto.FromECDSAPub(msgParams.Dst)), msgParams.Topic))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processServerAcceptedRequest processes incoming client requests of type:
|
||||||
|
// when client is ready to select the given node as its notification server
|
||||||
|
func (s *discoveryService) processServerAcceptedRequest(msg *whisper.ReceivedMessage) error {
|
||||||
|
var parsedMessage struct {
|
||||||
|
ServerID string `json:"server"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(msg.Payload, &parsedMessage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Src == nil {
|
||||||
|
return errors.New("message 'from' field is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that only requests made to the current node are processed
|
||||||
|
if parsedMessage.ServerID != `0x`+s.server.nodeID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// register client
|
||||||
|
sessionKey, err := s.server.RegisterClientSession(&ClientSession{
|
||||||
|
ClientKey: hex.EncodeToString(crypto.FromECDSAPub(msg.Src)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirm that client has been successfully subscribed
|
||||||
|
msgParams := whisper.MessageParams{
|
||||||
|
Src: s.server.protocolKey,
|
||||||
|
Dst: msg.Src,
|
||||||
|
Topic: MakeTopic([]byte(topicAckClientSubscription)),
|
||||||
|
Payload: []byte(`{"server": "0x` + s.server.nodeID + `", "key": "0x` + hex.EncodeToString(sessionKey) + `"}`),
|
||||||
|
TTL: uint32(s.server.config.TTL),
|
||||||
|
PoW: s.server.config.MinimumPoW,
|
||||||
|
WorkTime: 5,
|
||||||
|
}
|
||||||
|
response, err := whisper.NewSentMessage(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create server proposal message: %v", err)
|
||||||
|
}
|
||||||
|
env, err := response.Wrap(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to wrap server proposal message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.server.whisper.Send(env); err != nil {
|
||||||
|
return fmt.Errorf("failed to send server proposal message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(fmt.Sprintf("server confirms client subscription (dst: %v, topic: %x)", msgParams.Dst, msgParams.Topic))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
59
whisper/notifications/provider.go
Normal file
59
whisper/notifications/provider.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/status-im/status-go/geth/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationDeliveryProvider handles the notification delivery
|
||||||
|
type NotificationDeliveryProvider interface {
|
||||||
|
Send(id string, payload string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirebaseProvider represents FCM provider
|
||||||
|
type FirebaseProvider struct {
|
||||||
|
AuthorizationKey string
|
||||||
|
NotificationTriggerURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFirebaseProvider creates new FCM provider
|
||||||
|
func NewFirebaseProvider(config *params.FirebaseConfig) *FirebaseProvider {
|
||||||
|
authorizationKey, _ := config.ReadAuthorizationKeyFile()
|
||||||
|
return &FirebaseProvider{
|
||||||
|
NotificationTriggerURL: config.NotificationTriggerURL,
|
||||||
|
AuthorizationKey: string(authorizationKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send triggers sending of Push Notification to a given device id
|
||||||
|
func (p *FirebaseProvider) Send(id string, payload string) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
jsonRequest := strings.Replace(payload, "{{ ID }}", id, 3)
|
||||||
|
req, err := http.NewRequest("POST", p.NotificationTriggerURL, bytes.NewBuffer([]byte(jsonRequest)))
|
||||||
|
req.Header.Set("Authorization", "key="+p.AuthorizationKey)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
log.Debug("FCM response", "status", resp.Status, "header", resp.Header)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
log.Debug("FCM response body", "body", string(body))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
590
whisper/notifications/server.go
Normal file
590
whisper/notifications/server.go
Normal file
|
|
@ -0,0 +1,590 @@
|
||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||||
|
"github.com/status-im/status-go/geth/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
topicSendNotification = "SEND_NOTIFICATION"
|
||||||
|
topicNewChatSession = "NEW_CHAT_SESSION"
|
||||||
|
topicAckNewChatSession = "ACK_NEW_CHAT_SESSION"
|
||||||
|
topicNewDeviceRegistration = "NEW_DEVICE_REGISTRATION"
|
||||||
|
topicAckDeviceRegistration = "ACK_DEVICE_REGISTRATION"
|
||||||
|
topicCheckClientSession = "CHECK_CLIENT_SESSION"
|
||||||
|
topicConfirmClientSession = "CONFIRM_CLIENT_SESSION"
|
||||||
|
topicDropClientSession = "DROP_CLIENT_SESSION"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrServiceInitError = errors.New("notification service has not been properly initialized")
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationServer service capable of handling Push Notifications
|
||||||
|
type NotificationServer struct {
|
||||||
|
whisper *whisper.Whisper
|
||||||
|
config *params.WhisperConfig
|
||||||
|
|
||||||
|
nodeID string // proposed server will feature this ID
|
||||||
|
discovery *discoveryService // discovery service handles client/server negotiation, when server is selected
|
||||||
|
protocolKey *ecdsa.PrivateKey // private key of service, used to encode handshake communication
|
||||||
|
|
||||||
|
clientSessions map[string]*ClientSession
|
||||||
|
clientSessionsMu sync.RWMutex
|
||||||
|
|
||||||
|
chatSessions map[string]*ChatSession
|
||||||
|
chatSessionsMu sync.RWMutex
|
||||||
|
|
||||||
|
deviceSubscriptions map[string]*DeviceSubscription
|
||||||
|
deviceSubscriptionsMu sync.RWMutex
|
||||||
|
|
||||||
|
firebaseProvider NotificationDeliveryProvider
|
||||||
|
|
||||||
|
quit chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientSession abstracts notification client, which expects notifications whenever
|
||||||
|
// some envelope can be decoded with session key (key hash is compared for optimization)
|
||||||
|
type ClientSession struct {
|
||||||
|
ClientKey string // public key uniquely identifying a client
|
||||||
|
SessionKey []byte // actual symkey used for client - server communication
|
||||||
|
SessionKeyHash common.Hash // The Keccak256Hash of the symmetric key, which is shared between server/client
|
||||||
|
SessionKeyInput []byte // raw symkey used as input for actual SessionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatSession abstracts chat session, which some previously registered client can create.
|
||||||
|
// ChatSession is used by client for sharing common secret, allowing others to register
|
||||||
|
// themselves and eventually to trigger notifications.
|
||||||
|
type ChatSession struct {
|
||||||
|
ParentKey string // public key uniquely identifying a client session used to create a chat session
|
||||||
|
ChatKey string // ID that uniquely identifies a chat session
|
||||||
|
SessionKey []byte // actual symkey used for client - server communication
|
||||||
|
SessionKeyHash common.Hash // The Keccak256Hash of the symmetric key, which is shared between server/client
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceSubscription stores enough information about a device (or group of devices),
|
||||||
|
// so that Notification Server can trigger notification on that device(s)
|
||||||
|
type DeviceSubscription struct {
|
||||||
|
DeviceID string // ID that will be used as destination
|
||||||
|
ChatSessionKeyHash common.Hash // The Keccak256Hash of the symmetric key, which is shared between server/client
|
||||||
|
PubKey *ecdsa.PublicKey // public key of subscriber (to filter out when notification is triggered)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init used for service initialization, making sure it is safe to call Start()
|
||||||
|
func (s *NotificationServer) Init(whisperService *whisper.Whisper, whisperConfig *params.WhisperConfig) {
|
||||||
|
s.whisper = whisperService
|
||||||
|
s.config = whisperConfig
|
||||||
|
|
||||||
|
s.discovery = NewDiscoveryService(s)
|
||||||
|
s.clientSessions = make(map[string]*ClientSession)
|
||||||
|
s.chatSessions = make(map[string]*ChatSession)
|
||||||
|
s.deviceSubscriptions = make(map[string]*DeviceSubscription)
|
||||||
|
s.quit = make(chan struct{})
|
||||||
|
|
||||||
|
// setup providers (FCM only, for now)
|
||||||
|
s.firebaseProvider = NewFirebaseProvider(whisperConfig.FirebaseConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins notification loop, in a separate go routine
|
||||||
|
func (s *NotificationServer) Start(stack *p2p.Server) error {
|
||||||
|
if s.whisper == nil {
|
||||||
|
return ErrServiceInitError
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure nodeID
|
||||||
|
if stack != nil {
|
||||||
|
if nodeInfo := stack.NodeInfo(); nodeInfo != nil {
|
||||||
|
s.nodeID = nodeInfo.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure keys
|
||||||
|
identity, err := s.config.ReadIdentityFile()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.whisper.AddKeyPair(identity)
|
||||||
|
s.protocolKey = identity
|
||||||
|
log.Info("protocol pubkey", "key", common.ToHex(crypto.FromECDSAPub(&s.protocolKey.PublicKey)))
|
||||||
|
|
||||||
|
// start discovery protocol
|
||||||
|
s.discovery.Start()
|
||||||
|
|
||||||
|
// client session status requests
|
||||||
|
clientSessionStatusFilterID, err := s.installKeyFilter(topicCheckClientSession, s.protocolKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
go s.requestProcessorLoop(clientSessionStatusFilterID, topicDiscoverServer, s.processClientSessionStatusRequest)
|
||||||
|
|
||||||
|
// client session remove requests
|
||||||
|
dropClientSessionFilterID, err := s.installKeyFilter(topicDropClientSession, s.protocolKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
go s.requestProcessorLoop(dropClientSessionFilterID, topicDropClientSession, s.processDropClientSessionRequest)
|
||||||
|
|
||||||
|
log.Info("Whisper Notification Server started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop handles stopping the running notification loop, and all related resources
|
||||||
|
func (s *NotificationServer) Stop() error {
|
||||||
|
close(s.quit)
|
||||||
|
|
||||||
|
if s.whisper == nil {
|
||||||
|
return ErrServiceInitError
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.discovery != nil {
|
||||||
|
s.discovery.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Whisper Notification Server stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterClientSession forms a cryptographic link between server and client.
|
||||||
|
// It does so by sharing a session SymKey and installing filter listening for messages
|
||||||
|
// encrypted with that key. So, both server and client have a secure way to communicate.
|
||||||
|
func (s *NotificationServer) RegisterClientSession(session *ClientSession) (sessionKey []byte, err error) {
|
||||||
|
s.clientSessionsMu.Lock()
|
||||||
|
defer s.clientSessionsMu.Unlock()
|
||||||
|
|
||||||
|
// generate random symmetric session key
|
||||||
|
keyName := fmt.Sprintf("%s-%s", "ntfy-client", crypto.Keccak256Hash([]byte(session.ClientKey)).Hex())
|
||||||
|
sessionKey, sessionKeyDerived, err := s.makeSessionKey(keyName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate session key hash (will be used to match decrypted message to a given client id)
|
||||||
|
session.SessionKeyInput = sessionKey
|
||||||
|
session.SessionKeyHash = crypto.Keccak256Hash(sessionKeyDerived)
|
||||||
|
session.SessionKey = sessionKeyDerived
|
||||||
|
|
||||||
|
// append to list of known clients
|
||||||
|
// so that it is trivial to go key hash -> client session info
|
||||||
|
id := session.SessionKeyHash.Hex()
|
||||||
|
s.clientSessions[id] = session
|
||||||
|
|
||||||
|
// setup filter, which will get all incoming messages, that are encrypted with SymKey
|
||||||
|
filterID, err := s.installTopicFilter(topicNewChatSession, sessionKeyDerived)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
go s.requestProcessorLoop(filterID, topicNewChatSession, s.processNewChatSessionRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterChatSession forms a cryptographic link between server and client.
|
||||||
|
// This link is meant to be shared with other clients, so that they can use
|
||||||
|
// the shared SymKey to trigger notifications for devices attached to a given
|
||||||
|
// chat session.
|
||||||
|
func (s *NotificationServer) RegisterChatSession(session *ChatSession) (sessionKey []byte, err error) {
|
||||||
|
s.chatSessionsMu.Lock()
|
||||||
|
defer s.chatSessionsMu.Unlock()
|
||||||
|
|
||||||
|
// generate random symmetric session key
|
||||||
|
keyName := fmt.Sprintf("%s-%s", "ntfy-chat", crypto.Keccak256Hash([]byte(session.ParentKey+session.ChatKey)).Hex())
|
||||||
|
sessionKey, sessionKeyDerived, err := s.makeSessionKey(keyName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate session key hash (will be used to match decrypted message to a given client id)
|
||||||
|
session.SessionKeyHash = crypto.Keccak256Hash(sessionKeyDerived)
|
||||||
|
session.SessionKey = sessionKeyDerived
|
||||||
|
|
||||||
|
// append to list of known clients
|
||||||
|
// so that it is trivial to go key hash -> client session info
|
||||||
|
id := session.SessionKeyHash.Hex()
|
||||||
|
s.chatSessions[id] = session
|
||||||
|
|
||||||
|
// setup filter, to process incoming device registration requests
|
||||||
|
filterID1, err := s.installTopicFilter(topicNewDeviceRegistration, sessionKeyDerived)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
go s.requestProcessorLoop(filterID1, topicNewDeviceRegistration, s.processNewDeviceRegistrationRequest)
|
||||||
|
|
||||||
|
// setup filter, to process incoming notification trigger requests
|
||||||
|
filterID2, err := s.installTopicFilter(topicSendNotification, sessionKeyDerived)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
go s.requestProcessorLoop(filterID2, topicSendNotification, s.processSendNotificationRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterDeviceSubscription persists device id, so that it can be used to trigger notifications.
|
||||||
|
func (s *NotificationServer) RegisterDeviceSubscription(subscription *DeviceSubscription) error {
|
||||||
|
s.deviceSubscriptionsMu.Lock()
|
||||||
|
defer s.deviceSubscriptionsMu.Unlock()
|
||||||
|
|
||||||
|
// if one passes the same id again, we will just overwrite
|
||||||
|
id := fmt.Sprintf("%s-%s", "ntfy-device",
|
||||||
|
crypto.Keccak256Hash([]byte(subscription.ChatSessionKeyHash.Hex()+subscription.DeviceID)).Hex())
|
||||||
|
s.deviceSubscriptions[id] = subscription
|
||||||
|
|
||||||
|
log.Info("device registered", "device", subscription.DeviceID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropClientSession uninstalls session
|
||||||
|
func (s *NotificationServer) DropClientSession(id string) {
|
||||||
|
dropChatSessions := func(parentKey string) {
|
||||||
|
s.chatSessionsMu.Lock()
|
||||||
|
defer s.chatSessionsMu.Unlock()
|
||||||
|
|
||||||
|
for key, chatSession := range s.chatSessions {
|
||||||
|
if chatSession.ParentKey == parentKey {
|
||||||
|
delete(s.chatSessions, key)
|
||||||
|
log.Info("drop chat session", "key", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dropDeviceSubscriptions := func(parentKey string) {
|
||||||
|
s.deviceSubscriptionsMu.Lock()
|
||||||
|
defer s.deviceSubscriptionsMu.Unlock()
|
||||||
|
|
||||||
|
for key, subscription := range s.deviceSubscriptions {
|
||||||
|
if hex.EncodeToString(crypto.FromECDSAPub(subscription.PubKey)) == parentKey {
|
||||||
|
delete(s.deviceSubscriptions, key)
|
||||||
|
log.Info("drop device subscription", "key", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.clientSessionsMu.Lock()
|
||||||
|
if session, ok := s.clientSessions[id]; ok {
|
||||||
|
delete(s.clientSessions, id)
|
||||||
|
log.Info("server drops client session", "id", id)
|
||||||
|
s.clientSessionsMu.Unlock()
|
||||||
|
|
||||||
|
dropDeviceSubscriptions(session.ClientKey)
|
||||||
|
dropChatSessions(session.ClientKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processNewChatSessionRequest processes incoming client requests of type:
|
||||||
|
// client has a session key, and ready to create a new chat session (which is
|
||||||
|
// a bag of subscribed devices, basically)
|
||||||
|
func (s *NotificationServer) processNewChatSessionRequest(msg *whisper.ReceivedMessage) error {
|
||||||
|
s.clientSessionsMu.RLock()
|
||||||
|
defer s.clientSessionsMu.RUnlock()
|
||||||
|
|
||||||
|
var parsedMessage struct {
|
||||||
|
ChatID string `json:"chat"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(msg.Payload, &parsedMessage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Src == nil {
|
||||||
|
return errors.New("message 'from' field is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSession, ok := s.clientSessions[msg.SymKeyHash.Hex()]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("client session not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// register chat session
|
||||||
|
parentKey := hex.EncodeToString(crypto.FromECDSAPub(msg.Src))
|
||||||
|
sessionKey, err := s.RegisterChatSession(&ChatSession{
|
||||||
|
ParentKey: parentKey,
|
||||||
|
ChatKey: parsedMessage.ChatID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirm that chat has been successfully created
|
||||||
|
msgParams := whisper.MessageParams{
|
||||||
|
Dst: msg.Src,
|
||||||
|
KeySym: clientSession.SessionKey,
|
||||||
|
Topic: MakeTopic([]byte(topicAckNewChatSession)),
|
||||||
|
Payload: []byte(`{"server": "0x` + s.nodeID + `", "key": "0x` + hex.EncodeToString(sessionKey) + `"}`),
|
||||||
|
TTL: uint32(s.config.TTL),
|
||||||
|
PoW: s.config.MinimumPoW,
|
||||||
|
WorkTime: 5,
|
||||||
|
}
|
||||||
|
response, err := whisper.NewSentMessage(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create server response message: %v", err)
|
||||||
|
}
|
||||||
|
env, err := response.Wrap(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to wrap server response message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.whisper.Send(env); err != nil {
|
||||||
|
return fmt.Errorf("failed to send server response message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("server confirms chat creation", "dst",
|
||||||
|
common.ToHex(crypto.FromECDSAPub(msgParams.Dst)), "topic", msgParams.Topic.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processNewDeviceRegistrationRequest processes incoming client requests of type:
|
||||||
|
// client has a session key, creates chat, and obtains chat SymKey (to be shared with
|
||||||
|
// others). Then using that chat SymKey client registers it's device ID with server.
|
||||||
|
func (s *NotificationServer) processNewDeviceRegistrationRequest(msg *whisper.ReceivedMessage) error {
|
||||||
|
s.chatSessionsMu.RLock()
|
||||||
|
defer s.chatSessionsMu.RUnlock()
|
||||||
|
|
||||||
|
var parsedMessage struct {
|
||||||
|
DeviceID string `json:"device"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(msg.Payload, &parsedMessage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Src == nil {
|
||||||
|
return errors.New("message 'from' field is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
chatSession, ok := s.chatSessions[msg.SymKeyHash.Hex()]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("chat session not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsedMessage.DeviceID) <= 0 {
|
||||||
|
return errors.New("'device' cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// register chat session
|
||||||
|
err := s.RegisterDeviceSubscription(&DeviceSubscription{
|
||||||
|
DeviceID: parsedMessage.DeviceID,
|
||||||
|
ChatSessionKeyHash: chatSession.SessionKeyHash,
|
||||||
|
PubKey: msg.Src,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirm that client has been successfully subscribed
|
||||||
|
msgParams := whisper.MessageParams{
|
||||||
|
Dst: msg.Src,
|
||||||
|
KeySym: chatSession.SessionKey,
|
||||||
|
Topic: MakeTopic([]byte(topicAckDeviceRegistration)),
|
||||||
|
Payload: []byte(`{"server": "0x` + s.nodeID + `"}`),
|
||||||
|
TTL: uint32(s.config.TTL),
|
||||||
|
PoW: s.config.MinimumPoW,
|
||||||
|
WorkTime: 5,
|
||||||
|
}
|
||||||
|
response, err := whisper.NewSentMessage(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create server response message: %v", err)
|
||||||
|
}
|
||||||
|
env, err := response.Wrap(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to wrap server response message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.whisper.Send(env); err != nil {
|
||||||
|
return fmt.Errorf("failed to send server response message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("server confirms device registration", "dst",
|
||||||
|
common.ToHex(crypto.FromECDSAPub(msgParams.Dst)), "topic", msgParams.Topic.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processSendNotificationRequest processes incoming client requests of type:
|
||||||
|
// when client has session key, and ready to use it to send notifications
|
||||||
|
func (s *NotificationServer) processSendNotificationRequest(msg *whisper.ReceivedMessage) error {
|
||||||
|
s.deviceSubscriptionsMu.RLock()
|
||||||
|
defer s.deviceSubscriptionsMu.RUnlock()
|
||||||
|
|
||||||
|
for _, subscriber := range s.deviceSubscriptions {
|
||||||
|
if subscriber.ChatSessionKeyHash == msg.SymKeyHash {
|
||||||
|
if whisper.IsPubKeyEqual(msg.Src, subscriber.PubKey) {
|
||||||
|
continue // no need to notify ourselves
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.firebaseProvider != nil {
|
||||||
|
err := s.firebaseProvider.Send(subscriber.DeviceID, string(msg.Payload))
|
||||||
|
if err != nil {
|
||||||
|
log.Info("cannot send notification", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processClientSessionStatusRequest processes incoming client requests when:
|
||||||
|
// client wants to learn whether it is already registered on some of the servers
|
||||||
|
func (s *NotificationServer) processClientSessionStatusRequest(msg *whisper.ReceivedMessage) error {
|
||||||
|
s.clientSessionsMu.RLock()
|
||||||
|
defer s.clientSessionsMu.RUnlock()
|
||||||
|
|
||||||
|
if msg.Src == nil {
|
||||||
|
return errors.New("message 'from' field is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionKey []byte
|
||||||
|
pubKey := hex.EncodeToString(crypto.FromECDSAPub(msg.Src))
|
||||||
|
for _, clientSession := range s.clientSessions {
|
||||||
|
if clientSession.ClientKey == pubKey {
|
||||||
|
sessionKey = clientSession.SessionKeyInput
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// session is not found
|
||||||
|
if sessionKey == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// let client know that we have session for a given public key
|
||||||
|
msgParams := whisper.MessageParams{
|
||||||
|
Src: s.protocolKey,
|
||||||
|
Dst: msg.Src,
|
||||||
|
Topic: MakeTopic([]byte(topicConfirmClientSession)),
|
||||||
|
Payload: []byte(`{"server": "0x` + s.nodeID + `", "key": "0x` + hex.EncodeToString(sessionKey) + `"}`),
|
||||||
|
TTL: uint32(s.config.TTL),
|
||||||
|
PoW: s.config.MinimumPoW,
|
||||||
|
WorkTime: 5,
|
||||||
|
}
|
||||||
|
response, err := whisper.NewSentMessage(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create server response message: %v", err)
|
||||||
|
}
|
||||||
|
env, err := response.Wrap(&msgParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to wrap server response message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.whisper.Send(env); err != nil {
|
||||||
|
return fmt.Errorf("failed to send server response message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("server confirms client session", "dst",
|
||||||
|
common.ToHex(crypto.FromECDSAPub(msgParams.Dst)), "topic", msgParams.Topic.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processDropClientSessionRequest processes incoming client requests when:
|
||||||
|
// client wants to drop its sessions with notification servers (if they exist)
|
||||||
|
func (s *NotificationServer) processDropClientSessionRequest(msg *whisper.ReceivedMessage) error {
|
||||||
|
if msg.Src == nil {
|
||||||
|
return errors.New("message 'from' field is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.clientSessionsMu.RLock()
|
||||||
|
pubKey := hex.EncodeToString(crypto.FromECDSAPub(msg.Src))
|
||||||
|
for _, clientSession := range s.clientSessions {
|
||||||
|
if clientSession.ClientKey == pubKey {
|
||||||
|
s.clientSessionsMu.RUnlock()
|
||||||
|
s.DropClientSession(clientSession.SessionKeyHash.Hex())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// installTopicFilter installs Whisper filter using symmetric key
|
||||||
|
func (s *NotificationServer) installTopicFilter(topicName string, topicKey []byte) (filterID string, err error) {
|
||||||
|
topic := MakeTopicAsBytes([]byte(topicName))
|
||||||
|
filter := whisper.Filter{
|
||||||
|
KeySym: topicKey,
|
||||||
|
Topics: [][]byte{topic},
|
||||||
|
AllowP2P: true,
|
||||||
|
}
|
||||||
|
filterID, err = s.whisper.Subscribe(&filter)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug(fmt.Sprintf("installed topic filter %v for topic %x (%s)", filterID, topic, topicName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// installKeyFilter installs Whisper filter using asymmetric key
|
||||||
|
func (s *NotificationServer) installKeyFilter(topicName string, key *ecdsa.PrivateKey) (filterID string, err error) {
|
||||||
|
topic := MakeTopicAsBytes([]byte(topicName))
|
||||||
|
filter := whisper.Filter{
|
||||||
|
KeyAsym: key,
|
||||||
|
Topics: [][]byte{topic},
|
||||||
|
AllowP2P: true,
|
||||||
|
}
|
||||||
|
filterID, err = s.whisper.Subscribe(&filter)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed installing filter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(fmt.Sprintf("installed key filter %v for topic %x (%s)", filterID, topic, topicName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestProcessorLoop processes incoming client requests, by listening to a given filter,
|
||||||
|
// and executing process function on each incoming message
|
||||||
|
func (s *NotificationServer) requestProcessorLoop(filterID string, topicWatched string, fn messageProcessingFn) {
|
||||||
|
log.Debug(fmt.Sprintf("request processor started: %s", topicWatched))
|
||||||
|
|
||||||
|
filter := s.whisper.GetFilter(filterID)
|
||||||
|
if filter == nil {
|
||||||
|
log.Warn(fmt.Sprintf("filter is not installed: %s (for topic '%s')", filterID, topicWatched))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Millisecond * 50)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
messages := filter.Retrieve()
|
||||||
|
for _, msg := range messages {
|
||||||
|
if err := fn(msg); err != nil {
|
||||||
|
log.Warn("failed processing incoming request", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-s.quit:
|
||||||
|
log.Debug("request processor stopped", "topic", topicWatched)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeSessionKey generates and saves random SymKey, allowing to establish secure
|
||||||
|
// channel between server and client
|
||||||
|
func (s *NotificationServer) makeSessionKey(keyName string) (sessionKey, sessionKeyDerived []byte, err error) {
|
||||||
|
// wipe out previous occurrence of symmetric key
|
||||||
|
s.whisper.DeleteSymKey(keyName)
|
||||||
|
|
||||||
|
sessionKey, err = makeSessionKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyName, err = s.whisper.AddSymKey(keyName, sessionKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionKeyDerived, err = s.whisper.GetSymKey(keyName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
84
whisper/notifications/utils.go
Normal file
84
whisper/notifications/utils.go
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha512"
|
||||||
|
"errors"
|
||||||
|
"crypto/sha256"
|
||||||
|
|
||||||
|
crand "crypto/rand"
|
||||||
|
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// makeSessionKey returns pseudo-random symmetric key, which is used as
|
||||||
|
// session key between notification client and server
|
||||||
|
func makeSessionKey() ([]byte, error) {
|
||||||
|
// generate random key
|
||||||
|
const keyLen = 32
|
||||||
|
buf := make([]byte, keyLen)
|
||||||
|
_, err := crand.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !validateSymmetricKey(buf) {
|
||||||
|
return nil, errors.New("error in GenerateSymKey: crypto/rand failed to generate random data")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := buf[:keyLen]
|
||||||
|
derived, err := deriveKeyMaterial(key, whisper.EnvelopeVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !validateSymmetricKey(derived) {
|
||||||
|
return nil, errors.New("failed to derive valid key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return derived, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateSymmetricKey returns false if the key contains all zeros
|
||||||
|
func validateSymmetricKey(k []byte) bool {
|
||||||
|
return len(k) > 0 && !containsOnlyZeros(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsOnlyZeros checks if data is empty or not
|
||||||
|
func containsOnlyZeros(data []byte) bool {
|
||||||
|
for _, b := range data {
|
||||||
|
if b != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveKeyMaterial derives symmetric key material from the key or password./~~~
|
||||||
|
// pbkdf2 is used for security, in case people use password instead of randomly generated keys.
|
||||||
|
func deriveKeyMaterial(key []byte, version uint64) (derivedKey []byte, err error) {
|
||||||
|
if version == 0 {
|
||||||
|
// kdf should run no less than 0.1 seconds on average compute,
|
||||||
|
// because it's a once in a session experience
|
||||||
|
derivedKey := pbkdf2.Key(key, nil, 65356, 32, sha256.New)
|
||||||
|
return derivedKey, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("unknown version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeTopic returns Whisper topic *as bytes array* by generating cryptographic key from the provided password
|
||||||
|
func MakeTopicAsBytes(password []byte) ([]byte) {
|
||||||
|
topic := make([]byte, int(whisper.TopicLength))
|
||||||
|
x := pbkdf2.Key(password, password, 8196, 128, sha512.New)
|
||||||
|
for i := 0; i < len(x); i++ {
|
||||||
|
topic[i%whisper.TopicLength] ^= x[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return topic
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeTopic returns Whisper topic by generating cryptographic key from the provided password
|
||||||
|
func MakeTopic(password []byte) (topic whisper.TopicType) {
|
||||||
|
x := pbkdf2.Key(password, password, 8196, 128, sha512.New)
|
||||||
|
for i := 0; i < len(x); i++ {
|
||||||
|
topic[i%whisper.TopicLength] ^= x[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -134,6 +134,13 @@ func (self *Whisper) NewIdentity() *ecdsa.PrivateKey {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddIdentity adds identity into the known identities list (for message decryption).
|
||||||
|
func (self *Whisper) AddIdentity(key *ecdsa.PrivateKey) {
|
||||||
|
self.keysMu.Lock()
|
||||||
|
self.keys[string(crypto.FromECDSAPub(&key.PublicKey))] = key
|
||||||
|
self.keysMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// HasIdentity checks if the the whisper node is configured with the private key
|
// HasIdentity checks if the the whisper node is configured with the private key
|
||||||
// of the specified public pair.
|
// of the specified public pair.
|
||||||
func (self *Whisper) HasIdentity(key *ecdsa.PublicKey) bool {
|
func (self *Whisper) HasIdentity(key *ecdsa.PublicKey) bool {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/common/message"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
|
@ -238,13 +239,17 @@ type newMessageOverride struct {
|
||||||
// Post a message on the Whisper network.
|
// Post a message on the Whisper network.
|
||||||
func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, error) {
|
func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, error) {
|
||||||
var (
|
var (
|
||||||
symKeyGiven = len(req.SymKeyID) > 0
|
symKeyGiven = len(req.SymKeyID) > 0
|
||||||
pubKeyGiven = len(req.PublicKey) > 0
|
pubKeyGiven = len(req.PublicKey) > 0
|
||||||
err error
|
isP2PMessage = len(req.TargetPeer) > 0
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.PendingStatus, &req, nil, nil, nil)
|
||||||
|
|
||||||
// user must specify either a symmetric or an asymmetric key
|
// user must specify either a symmetric or an asymmetric key
|
||||||
if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) {
|
if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrSymAsym)
|
||||||
return false, ErrSymAsym
|
return false, ErrSymAsym
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,6 +265,7 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er
|
||||||
// Set key that is used to sign the message
|
// Set key that is used to sign the message
|
||||||
if len(req.Sig) > 0 {
|
if len(req.Sig) > 0 {
|
||||||
if params.Src, err = api.w.GetPrivateKey(req.Sig); err != nil {
|
if params.Src, err = api.w.GetPrivateKey(req.Sig); err != nil {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -267,12 +273,15 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er
|
||||||
// Set symmetric key that is used to encrypt the message
|
// Set symmetric key that is used to encrypt the message
|
||||||
if symKeyGiven {
|
if symKeyGiven {
|
||||||
if params.Topic == (TopicType{}) { // topics are mandatory with symmetric encryption
|
if params.Topic == (TopicType{}) { // topics are mandatory with symmetric encryption
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrNoTopics)
|
||||||
return false, ErrNoTopics
|
return false, ErrNoTopics
|
||||||
}
|
}
|
||||||
if params.KeySym, err = api.w.GetSymKey(req.SymKeyID); err != nil {
|
if params.KeySym, err = api.w.GetSymKey(req.SymKeyID); err != nil {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if !validateSymmetricKey(params.KeySym) {
|
if !validateSymmetricKey(params.KeySym) {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrInvalidSymmetricKey)
|
||||||
return false, ErrInvalidSymmetricKey
|
return false, ErrInvalidSymmetricKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,6 +290,7 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er
|
||||||
if pubKeyGiven {
|
if pubKeyGiven {
|
||||||
params.Dst = crypto.ToECDSAPub(req.PublicKey)
|
params.Dst = crypto.ToECDSAPub(req.PublicKey)
|
||||||
if !ValidatePublicKey(params.Dst) {
|
if !ValidatePublicKey(params.Dst) {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrInvalidPublicKey)
|
||||||
return false, ErrInvalidPublicKey
|
return false, ErrInvalidPublicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -288,11 +298,13 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er
|
||||||
// encrypt and sent message
|
// encrypt and sent message
|
||||||
whisperMsg, err := NewSentMessage(params)
|
whisperMsg, err := NewSentMessage(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
env, err := whisperMsg.Wrap(params)
|
env, err := whisperMsg.Wrap(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,19 +312,41 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er
|
||||||
if len(req.TargetPeer) > 0 {
|
if len(req.TargetPeer) > 0 {
|
||||||
n, err := discover.ParseNode(req.TargetPeer)
|
n, err := discover.ParseNode(req.TargetPeer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, env, nil, err)
|
||||||
return false, fmt.Errorf("failed to parse target peer: %s", err)
|
return false, fmt.Errorf("failed to parse target peer: %s", err)
|
||||||
}
|
}
|
||||||
return true, api.w.SendP2PMessage(n.ID[:], env)
|
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.SentStatus, &req, env, nil, nil)
|
||||||
|
|
||||||
|
if err := api.w.SendP2PMessage(n.ID[:], env); err != nil {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, env, nil, err)
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.DeliveredStatus, &req, env, nil, err)
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that the message PoW meets the node's minimum accepted PoW
|
// ensure that the message PoW meets the node's minimum accepted PoW
|
||||||
if req.PowTarget < api.w.MinPow() {
|
if req.PowTarget < api.w.MinPow() {
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, env, nil, ErrTooLowPoW)
|
||||||
return false, ErrTooLowPoW
|
return false, ErrTooLowPoW
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.w.traceOutgoingDelivery(isP2PMessage, message.SentStatus, &req, env, nil, nil)
|
||||||
return true, api.w.Send(env)
|
return true, api.w.Send(env)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UninstallFilter is alias for Unsubscribe
|
||||||
|
func (api *PublicWhisperAPI) UninstallFilter(id string) {
|
||||||
|
api.w.Unsubscribe(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe disables and removes an existing filter.
|
||||||
|
func (api *PublicWhisperAPI) Unsubscribe(id string) {
|
||||||
|
api.w.Unsubscribe(id)
|
||||||
|
}
|
||||||
|
|
||||||
//go:generate gencodec -type Criteria -field-override criteriaOverride -out gen_criteria_json.go
|
//go:generate gencodec -type Criteria -field-override criteriaOverride -out gen_criteria_json.go
|
||||||
|
|
||||||
// Criteria holds various filter options for inbound messages.
|
// Criteria holds various filter options for inbound messages.
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ package whisperv5
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/message"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -57,7 +60,7 @@ const (
|
||||||
|
|
||||||
MaxMessageSize = uint32(10 * 1024 * 1024) // maximum accepted size of a message.
|
MaxMessageSize = uint32(10 * 1024 * 1024) // maximum accepted size of a message.
|
||||||
DefaultMaxMessageSize = uint32(1024 * 1024)
|
DefaultMaxMessageSize = uint32(1024 * 1024)
|
||||||
DefaultMinimumPoW = 0.2
|
DefaultMinimumPoW = 0.001
|
||||||
|
|
||||||
padSizeLimit = 256 // just an arbitrary number, could be changed without breaking the protocol (must not exceed 2^24)
|
padSizeLimit = 256 // just an arbitrary number, could be changed without breaking the protocol (must not exceed 2^24)
|
||||||
messageQueueLimit = 1024
|
messageQueueLimit = 1024
|
||||||
|
|
@ -85,3 +88,35 @@ type MailServer interface {
|
||||||
Archive(env *Envelope)
|
Archive(env *Envelope)
|
||||||
DeliverMail(whisperPeer *Peer, request *Envelope)
|
DeliverMail(whisperPeer *Peer, request *Envelope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotificationServer represents a notification server,
|
||||||
|
// capable of screening incoming envelopes for special
|
||||||
|
// topics, and once located, subscribe client nodes as
|
||||||
|
// recipients to notifications (push notifications atm)
|
||||||
|
type NotificationServer interface {
|
||||||
|
// Start initializes notification sending loop
|
||||||
|
Start(server *p2p.Server) error
|
||||||
|
|
||||||
|
// Stop stops notification sending loop, releasing related resources
|
||||||
|
Stop() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageState holds the current delivery status of a whisper p2p message.
|
||||||
|
type MessageState struct {
|
||||||
|
IsP2P bool `json:"is_p2p"`
|
||||||
|
Reason error `json:"reason"`
|
||||||
|
Envelope Envelope `json:"envelope"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Source NewMessage `json:"source"`
|
||||||
|
Status message.Status `json:"status"`
|
||||||
|
Direction message.Direction `json:"direction"`
|
||||||
|
Received ReceivedMessage `json:"received"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeliveryServer represents a small message status
|
||||||
|
// notification system where a message delivery status
|
||||||
|
// update event is delivered to it's underline system
|
||||||
|
// for both rpc messages and p2p messages.
|
||||||
|
type DeliveryServer interface {
|
||||||
|
SendState(MessageState)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,12 @@ package whisperv5
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/message"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
|
|
@ -115,15 +117,20 @@ func (fs *Filters) NotifyWatchers(env *Envelope, p2pMessage bool) {
|
||||||
if match {
|
if match {
|
||||||
msg = env.Open(watcher)
|
msg = env.Open(watcher)
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
|
err := errors.New("Envelope failed to be opened")
|
||||||
|
fs.whisper.traceIncomingDelivery(p2pMessage, message.RejectedStatus, nil, env, nil, err)
|
||||||
log.Trace("processing message: failed to open", "message", env.Hash().Hex(), "filter", i)
|
log.Trace("processing message: failed to open", "message", env.Hash().Hex(), "filter", i)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
err := errors.New("processing message: does not match")
|
||||||
|
fs.whisper.traceIncomingDelivery(p2pMessage, message.RejectedStatus, nil, env, nil, err)
|
||||||
log.Trace("processing message: does not match", "message", env.Hash().Hex(), "filter", i)
|
log.Trace("processing message: does not match", "message", env.Hash().Hex(), "filter", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if match && msg != nil {
|
if match && msg != nil {
|
||||||
log.Trace("processing message: decrypted", "hash", env.Hash().Hex())
|
log.Trace("processing message: decrypted", "hash", env.Hash().Hex())
|
||||||
|
fs.whisper.traceIncomingDelivery(p2pMessage, message.DeliveredStatus, nil, env, msg, nil)
|
||||||
if watcher.Src == nil || IsPubKeyEqual(msg.Src, watcher.Src) {
|
if watcher.Src == nil || IsPubKeyEqual(msg.Src, watcher.Src) {
|
||||||
watcher.Trigger(msg)
|
watcher.Trigger(msg)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/message"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
|
@ -77,7 +78,9 @@ type Whisper struct {
|
||||||
statsMu sync.Mutex // guard stats
|
statsMu sync.Mutex // guard stats
|
||||||
stats Statistics // Statistics of whisper node
|
stats Statistics // Statistics of whisper node
|
||||||
|
|
||||||
mailServer MailServer // MailServer interface
|
mailServer MailServer // MailServer interface
|
||||||
|
deliveryServer DeliveryServer // DeliveryServer interface
|
||||||
|
notificationServer NotificationServer
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a Whisper client ready to communicate through the Ethereum P2P network.
|
// New creates a Whisper client ready to communicate through the Ethereum P2P network.
|
||||||
|
|
@ -156,6 +159,16 @@ func (w *Whisper) RegisterServer(server MailServer) {
|
||||||
w.mailServer = server
|
w.mailServer = server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterDeliveryServer registers notification server with Whisper
|
||||||
|
func (w *Whisper) RegisterDeliveryServer(server DeliveryServer) {
|
||||||
|
w.deliveryServer = server
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterNotificationServer registers notification server with Whisper
|
||||||
|
func (w *Whisper) RegisterNotificationServer(server NotificationServer) {
|
||||||
|
w.notificationServer = server
|
||||||
|
}
|
||||||
|
|
||||||
// Protocols returns the whisper sub-protocols ran by this particular client.
|
// Protocols returns the whisper sub-protocols ran by this particular client.
|
||||||
func (w *Whisper) Protocols() []p2p.Protocol {
|
func (w *Whisper) Protocols() []p2p.Protocol {
|
||||||
return []p2p.Protocol{w.protocol}
|
return []p2p.Protocol{w.protocol}
|
||||||
|
|
@ -250,9 +263,9 @@ func (w *Whisper) NewKeyPair() (string, error) {
|
||||||
return "", fmt.Errorf("failed to generate valid key")
|
return "", fmt.Errorf("failed to generate valid key")
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := GenerateRandomID()
|
id, err := toDeterministicID(common.ToHex(crypto.FromECDSAPub(&key.PublicKey)), keyIdSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to generate ID: %s", err)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.keyMu.Lock()
|
w.keyMu.Lock()
|
||||||
|
|
@ -265,45 +278,94 @@ func (w *Whisper) NewKeyPair() (string, error) {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteKeyPair deletes the specified key if it exists.
|
// AddIdentity adds cryptographic identity into the known
|
||||||
func (w *Whisper) DeleteKeyPair(key string) bool {
|
// identities list (for message decryption).
|
||||||
|
func (w *Whisper) AddKeyPair(key *ecdsa.PrivateKey) (string, error) {
|
||||||
|
id, err := makeDeterministicID(common.ToHex(crypto.FromECDSAPub(&key.PublicKey)), keyIdSize)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if w.HasKeyPair(id) {
|
||||||
|
return id, nil // no need to re-inject
|
||||||
|
}
|
||||||
|
|
||||||
w.keyMu.Lock()
|
w.keyMu.Lock()
|
||||||
defer w.keyMu.Unlock()
|
defer w.keyMu.Unlock()
|
||||||
|
|
||||||
if w.privateKeys[key] != nil {
|
w.privateKeys[id] = key
|
||||||
delete(w.privateKeys, key)
|
log.Info("Whisper identity added", "id", id, "pubkey", common.ToHex(crypto.FromECDSAPub(&key.PublicKey)))
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectKeyPair adds cryptographic identity, and makes sure
|
||||||
|
// that it is the only private key known to the node.
|
||||||
|
func (w *Whisper) SelectKeyPair(key *ecdsa.PrivateKey) error {
|
||||||
|
id, err := makeDeterministicID(common.ToHex(crypto.FromECDSAPub(&key.PublicKey)), keyIdSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.keyMu.Lock()
|
||||||
|
defer w.keyMu.Unlock()
|
||||||
|
|
||||||
|
w.privateKeys = make(map[string]*ecdsa.PrivateKey) // reset key store
|
||||||
|
w.privateKeys[id] = key
|
||||||
|
|
||||||
|
log.Info("Whisper identity selected", "id", id, "key", common.ToHex(crypto.FromECDSAPub(&key.PublicKey)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteKeyPairs removes all cryptographic identities known to the node
|
||||||
|
func (w *Whisper) DeleteKeyPairs() error {
|
||||||
|
w.keyMu.Lock()
|
||||||
|
defer w.keyMu.Unlock()
|
||||||
|
|
||||||
|
w.privateKeys = make(map[string]*ecdsa.PrivateKey)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteKeyPair deletes the specified key if it exists.
|
||||||
|
func (w *Whisper) DeleteKeyPair(id string) bool {
|
||||||
|
deterministicID, err := toDeterministicID(id, keyIdSize)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
w.keyMu.Lock()
|
||||||
|
defer w.keyMu.Unlock()
|
||||||
|
|
||||||
|
if w.privateKeys[deterministicID] != nil {
|
||||||
|
delete(w.privateKeys, deterministicID)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddKeyPair imports a asymmetric private key and returns it identifier.
|
|
||||||
func (w *Whisper) AddKeyPair(key *ecdsa.PrivateKey) (string, error) {
|
|
||||||
id, err := GenerateRandomID()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to generate ID: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.keyMu.Lock()
|
|
||||||
w.privateKeys[id] = key
|
|
||||||
w.keyMu.Unlock()
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasKeyPair checks if the the whisper node is configured with the private key
|
// HasKeyPair checks if the the whisper node is configured with the private key
|
||||||
// of the specified public pair.
|
// of the specified public pair.
|
||||||
func (w *Whisper) HasKeyPair(id string) bool {
|
func (w *Whisper) HasKeyPair(id string) bool {
|
||||||
|
deterministicID, err := toDeterministicID(id, keyIdSize)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
w.keyMu.RLock()
|
w.keyMu.RLock()
|
||||||
defer w.keyMu.RUnlock()
|
defer w.keyMu.RUnlock()
|
||||||
return w.privateKeys[id] != nil
|
return w.privateKeys[deterministicID] != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPrivateKey retrieves the private key of the specified identity.
|
// GetPrivateKey retrieves the private key of the specified identity.
|
||||||
func (w *Whisper) GetPrivateKey(id string) (*ecdsa.PrivateKey, error) {
|
func (w *Whisper) GetPrivateKey(id string) (*ecdsa.PrivateKey, error) {
|
||||||
|
deterministicID, err := toDeterministicID(id, keyIdSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
w.keyMu.RLock()
|
w.keyMu.RLock()
|
||||||
defer w.keyMu.RUnlock()
|
defer w.keyMu.RUnlock()
|
||||||
key := w.privateKeys[id]
|
key := w.privateKeys[deterministicID]
|
||||||
if key == nil {
|
if key == nil {
|
||||||
return nil, fmt.Errorf("invalid id")
|
return nil, fmt.Errorf("invalid id")
|
||||||
}
|
}
|
||||||
|
|
@ -336,6 +398,23 @@ func (w *Whisper) GenerateSymKey() (string, error) {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddSymKey stores the key with a given id.
|
||||||
|
func (w *Whisper) AddSymKey(id string, key []byte) (string, error) {
|
||||||
|
deterministicID, err := toDeterministicID(id, keyIdSize)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.keyMu.Lock()
|
||||||
|
defer w.keyMu.Unlock()
|
||||||
|
|
||||||
|
if w.symKeys[deterministicID] != nil {
|
||||||
|
return "", fmt.Errorf("key already exists: %v", id)
|
||||||
|
}
|
||||||
|
w.symKeys[deterministicID] = key
|
||||||
|
return deterministicID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddSymKeyDirect stores the key, and returns its id.
|
// AddSymKeyDirect stores the key, and returns its id.
|
||||||
func (w *Whisper) AddSymKeyDirect(key []byte) (string, error) {
|
func (w *Whisper) AddSymKeyDirect(key []byte) (string, error) {
|
||||||
if len(key) != aesKeyLength {
|
if len(key) != aesKeyLength {
|
||||||
|
|
@ -447,7 +526,7 @@ func (w *Whisper) Send(envelope *Envelope) error {
|
||||||
|
|
||||||
// Start implements node.Service, starting the background data propagation thread
|
// Start implements node.Service, starting the background data propagation thread
|
||||||
// of the Whisper protocol.
|
// of the Whisper protocol.
|
||||||
func (w *Whisper) Start(*p2p.Server) error {
|
func (w *Whisper) Start(stack *p2p.Server) error {
|
||||||
log.Info("started whisper v." + ProtocolVersionStr)
|
log.Info("started whisper v." + ProtocolVersionStr)
|
||||||
go w.update()
|
go w.update()
|
||||||
|
|
||||||
|
|
@ -456,6 +535,12 @@ func (w *Whisper) Start(*p2p.Server) error {
|
||||||
go w.processQueue()
|
go w.processQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if w.notificationServer != nil {
|
||||||
|
if err := w.notificationServer.Start(stack); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -463,6 +548,13 @@ func (w *Whisper) Start(*p2p.Server) error {
|
||||||
// of the Whisper protocol.
|
// of the Whisper protocol.
|
||||||
func (w *Whisper) Stop() error {
|
func (w *Whisper) Stop() error {
|
||||||
close(w.quit)
|
close(w.quit)
|
||||||
|
|
||||||
|
if w.notificationServer != nil {
|
||||||
|
if err := w.notificationServer.Stop(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("whisper stopped")
|
log.Info("whisper stopped")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -535,8 +627,11 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
|
||||||
var envelope Envelope
|
var envelope Envelope
|
||||||
if err := packet.Decode(&envelope); err != nil {
|
if err := packet.Decode(&envelope); err != nil {
|
||||||
log.Warn("failed to decode direct message, peer will be disconnected", "peer", p.peer.ID(), "err", err)
|
log.Warn("failed to decode direct message, peer will be disconnected", "peer", p.peer.ID(), "err", err)
|
||||||
|
wh.traceIncomingDelivery(true, message.RejectedStatus, nil, &envelope, nil, err)
|
||||||
return errors.New("invalid direct message")
|
return errors.New("invalid direct message")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wh.traceIncomingDelivery(true, message.SentStatus, nil, &envelope, nil, nil)
|
||||||
wh.postEvent(&envelope, true)
|
wh.postEvent(&envelope, true)
|
||||||
}
|
}
|
||||||
case p2pRequestCode:
|
case p2pRequestCode:
|
||||||
|
|
@ -545,6 +640,7 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
|
||||||
var request Envelope
|
var request Envelope
|
||||||
if err := packet.Decode(&request); err != nil {
|
if err := packet.Decode(&request); err != nil {
|
||||||
log.Warn("failed to decode p2p request message, peer will be disconnected", "peer", p.peer.ID(), "err", err)
|
log.Warn("failed to decode p2p request message, peer will be disconnected", "peer", p.peer.ID(), "err", err)
|
||||||
|
wh.traceIncomingDelivery(true, message.RejectedStatus, nil, &request, nil, err)
|
||||||
return errors.New("invalid p2p request")
|
return errors.New("invalid p2p request")
|
||||||
}
|
}
|
||||||
wh.mailServer.DeliverMail(p, &request)
|
wh.mailServer.DeliverMail(p, &request)
|
||||||
|
|
@ -615,16 +711,22 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) {
|
||||||
if !wh.expirations[envelope.Expiry].Has(hash) {
|
if !wh.expirations[envelope.Expiry].Has(hash) {
|
||||||
wh.expirations[envelope.Expiry].Add(hash)
|
wh.expirations[envelope.Expiry].Add(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wh.traceIncomingDelivery(false, message.CachedStatus, nil, envelope, nil, nil)
|
||||||
}
|
}
|
||||||
wh.poolMu.Unlock()
|
wh.poolMu.Unlock()
|
||||||
|
|
||||||
if alreadyCached {
|
if alreadyCached {
|
||||||
log.Trace("whisper envelope already cached", "hash", envelope.Hash().Hex())
|
log.Trace("whisper envelope already cached", "hash", envelope.Hash().Hex())
|
||||||
|
wh.traceIncomingDelivery(false, message.ResentStatus, nil, envelope, nil, nil)
|
||||||
} else {
|
} else {
|
||||||
log.Trace("cached whisper envelope", "hash", envelope.Hash().Hex())
|
log.Trace("cached whisper envelope", "hash", envelope.Hash().Hex())
|
||||||
wh.statsMu.Lock()
|
wh.statsMu.Lock()
|
||||||
wh.stats.memoryUsed += envelope.size()
|
wh.stats.memoryUsed += envelope.size()
|
||||||
wh.statsMu.Unlock()
|
wh.statsMu.Unlock()
|
||||||
|
|
||||||
|
wh.traceIncomingDelivery(false, message.QueuedStatus, nil, envelope, nil, nil)
|
||||||
|
|
||||||
wh.postEvent(envelope, false) // notify the local node about the new message
|
wh.postEvent(envelope, false) // notify the local node about the new message
|
||||||
if wh.mailServer != nil {
|
if wh.mailServer != nil {
|
||||||
wh.mailServer.Archive(envelope)
|
wh.mailServer.Archive(envelope)
|
||||||
|
|
@ -633,6 +735,47 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Whisper) traceIncomingDelivery(isP2P bool, status message.Status, src *NewMessage, env *Envelope, rec *ReceivedMessage, err error) {
|
||||||
|
w.traceDelivery(isP2P, message.IncomingMessage, status, src, env, rec, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Whisper) traceOutgoingDelivery(isP2P bool, status message.Status, src *NewMessage, env *Envelope, rec *ReceivedMessage, err error) {
|
||||||
|
w.traceDelivery(isP2P, message.OutgoingMessage, status, src, env, rec, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Whisper) traceDelivery(isP2P bool, dir message.Direction, status message.Status, newmsg *NewMessage, envelope *Envelope, received *ReceivedMessage, err error) {
|
||||||
|
if w.deliveryServer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var env Envelope
|
||||||
|
var rec ReceivedMessage
|
||||||
|
var src NewMessage
|
||||||
|
|
||||||
|
if newmsg != nil {
|
||||||
|
src = *newmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
if envelope != nil {
|
||||||
|
env = *envelope
|
||||||
|
}
|
||||||
|
|
||||||
|
if received != nil {
|
||||||
|
rec = *received
|
||||||
|
}
|
||||||
|
|
||||||
|
go w.deliveryServer.SendState(MessageState{
|
||||||
|
Reason: err,
|
||||||
|
Source: src,
|
||||||
|
Received: rec,
|
||||||
|
IsP2P: isP2P,
|
||||||
|
Status: status,
|
||||||
|
Envelope: env,
|
||||||
|
Direction: dir,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// postEvent queues the message for further processing.
|
// postEvent queues the message for further processing.
|
||||||
func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) {
|
func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) {
|
||||||
// if the version of incoming message is higher than
|
// if the version of incoming message is higher than
|
||||||
|
|
@ -645,6 +788,13 @@ func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) {
|
||||||
w.checkOverflow()
|
w.checkOverflow()
|
||||||
w.messageQueue <- envelope
|
w.messageQueue <- envelope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.deliveryServer != nil {
|
||||||
|
err := fmt.Errorf("Mismatch Envelope version(%d) to wanted Version(%d)", envelope.Ver(), EnvelopeVersion)
|
||||||
|
w.traceIncomingDelivery(isP2P, message.RejectedStatus, nil, envelope, nil, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -674,9 +824,11 @@ func (w *Whisper) processQueue() {
|
||||||
return
|
return
|
||||||
|
|
||||||
case e = <-w.messageQueue:
|
case e = <-w.messageQueue:
|
||||||
|
w.traceIncomingDelivery(false, message.ProcessingStatus, nil, e, nil, nil)
|
||||||
w.filters.NotifyWatchers(e, false)
|
w.filters.NotifyWatchers(e, false)
|
||||||
|
|
||||||
case e = <-w.p2pMsgQueue:
|
case e = <-w.p2pMsgQueue:
|
||||||
|
w.traceIncomingDelivery(true, message.ProcessingStatus, nil, e, nil, nil)
|
||||||
w.filters.NotifyWatchers(e, true)
|
w.filters.NotifyWatchers(e, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -856,3 +1008,30 @@ func GenerateRandomID() (id string, err error) {
|
||||||
id = common.Bytes2Hex(buf)
|
id = common.Bytes2Hex(buf)
|
||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// makeDeterministicID generates a deterministic ID, based on a given input
|
||||||
|
func makeDeterministicID(input string, keyLen int) (id string, err error) {
|
||||||
|
buf := pbkdf2.Key([]byte(input), nil, 4096, keyLen, sha256.New)
|
||||||
|
if !validateSymmetricKey(buf) {
|
||||||
|
return "", fmt.Errorf("error in GenerateDeterministicID: failed to generate key")
|
||||||
|
}
|
||||||
|
id = common.Bytes2Hex(buf)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// toDeterministicID reviews incoming id, and transforms it to format
|
||||||
|
// expected internally be private key store. Originally, public keys
|
||||||
|
// were used as keys, now random keys are being used. And in order to
|
||||||
|
// make it easier to consume, we now allow both random IDs and public
|
||||||
|
// keys to be passed.
|
||||||
|
func toDeterministicID(id string, expectedLen int) (string, error) {
|
||||||
|
if len(id) != (expectedLen * 2) { // we received hex key, so number of chars in id is doubled
|
||||||
|
var err error
|
||||||
|
id, err = makeDeterministicID(id, expectedLen)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue