Repository: threefoldtech/zos
Pull request #993 (2020-10-07 14:29:21)

add new method to de/encrypt msg based on a ECDH algorithm
associated PR: 
- explorer https://github.com/threefoldtech/tfexplorer/pull/184
- jsng client https://github.com/threefoldtech/js-sdk/pull/1398


Patch:
diff --git a/go.mod b/go.mod
index 2806979b..5929b237 100644
--- a/go.mod
+++ b/go.mod
@@ -61,7 +61,7 @@ require (
 	github.com/stretchr/testify v1.6.1
 	github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
 	github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae
-	github.com/threefoldtech/tfexplorer v0.4.1-0.20201006092040-c7f28e901ea5
+	github.com/threefoldtech/tfexplorer v0.4.1-0.20201008114938-260c3694ef48
 	github.com/threefoldtech/zbus v0.1.3
 	github.com/urfave/cli v1.22.4
 	github.com/vishvananda/netlink v1.1.0
diff --git a/go.sum b/go.sum
index ac0b7dad..9299fe4a 100644
--- a/go.sum
+++ b/go.sum
@@ -604,6 +604,8 @@ github.com/threefoldtech/tfexplorer v0.4.1-0.20200930141942-5d5dac62dca0 h1:K7XP
 github.com/threefoldtech/tfexplorer v0.4.1-0.20200930141942-5d5dac62dca0/go.mod h1:0261YnFlaQNDv1hNzdJc/2DuYq9B9qe9D6QnZuZOCh0=
 github.com/threefoldtech/tfexplorer v0.4.1-0.20201006092040-c7f28e901ea5 h1:72ioWTU8nqW9VpYBT++LOddh6aZ7ZFNctxD0ESM0ffk=
 github.com/threefoldtech/tfexplorer v0.4.1-0.20201006092040-c7f28e901ea5/go.mod h1:0261YnFlaQNDv1hNzdJc/2DuYq9B9qe9D6QnZuZOCh0=
+github.com/threefoldtech/tfexplorer v0.4.1-0.20201008114938-260c3694ef48 h1:EADdhzDfiRYpddc9dSznO5nzg7hTyckUZn2NRoE0WT8=
+github.com/threefoldtech/tfexplorer v0.4.1-0.20201008114938-260c3694ef48/go.mod h1:0261YnFlaQNDv1hNzdJc/2DuYq9B9qe9D6QnZuZOCh0=
 github.com/threefoldtech/zbus v0.1.3 h1:18DnIzximRbATle5ZdZz0i84n/bCYB8k/gkhr2dXayc=
 github.com/threefoldtech/zbus v0.1.3/go.mod h1:ZtiRpcqzEBJetVQDsEbw0p48h/AF3O1kf0tvd30I0BU=
 github.com/threefoldtech/zos v0.2.4-rc2/go.mod h1:7A2oflcmSVsHFC4slOcydWgJyFBMFMH9wsaTRv+CnTA=
diff --git a/pkg/crypto/encryption.go b/pkg/crypto/encryption.go
index 09cb8281..132344d4 100644
--- a/pkg/crypto/encryption.go
+++ b/pkg/crypto/encryption.go
@@ -1,9 +1,14 @@
 package crypto
 
 import (
+	"crypto/ed25519"
+	"crypto/rand"
+	"fmt"
+
 	"github.com/agl/ed25519/extra25519"
 	box "github.com/whs/nacl-sealed-box"
-	"golang.org/x/crypto/ed25519"
+	"golang.org/x/crypto/curve25519"
+	"golang.org/x/crypto/nacl/secretbox"
 )
 
 // Encrypt encrypts msg with a cure25519 public key derived from an ed25519 public key
@@ -39,3 +44,56 @@ func PublicKeyToCurve25519(pk ed25519.PublicKey) [32]byte {
 	extra25519.PublicKeyToCurve25519(&curvePub, &edPriv)
 	return curvePub
 }
+
+// DecryptECDH decrypt aes encrypted msg using a shared key derived from sk and pk using Elliptic curve Diffie Helman algorithm
+func DecryptECDH(msg []byte, sk ed25519.PrivateKey, pk ed25519.PublicKey) ([]byte, error) {
+
+	sharedSecretBytes, err := sharedSecret(sk, pk)
+	if err != nil {
+		return nil, err
+	}
+	var key [32]byte
+	copy(key[:], sharedSecretBytes)
+
+	var nonce [24]byte
+	copy(nonce[:], msg[:24])
+
+	descrypted, ok := secretbox.Open(nil, msg[24:], &nonce, &key)
+	if !ok {
+		return nil, fmt.Errorf("decryption error")
+	}
+
+	return descrypted, nil
+}
+
+// EncryptECDH aes encrypt msg using a shared key derived from sk and pk using Elliptic curve Diffie Helman algorithm
+// the nonce if prepended to the encrypted message
+func EncryptECDH(msg []byte, sk ed25519.PrivateKey, pk ed25519.PublicKey) ([]byte, error) {
+
+	sharedSecretBytes, err := sharedSecret(sk, pk)
+	if err != nil {
+		return nil, err
+	}
+	var key [32]byte
+	copy(key[:], sharedSecretBytes)
+
+	var nonce [24]byte
+	if _, err = rand.Read(nonce[:]); err != nil {
+		return nil, err
+	}
+
+	encrypted := secretbox.Seal(nonce[:], msg, &nonce, &key)
+	return encrypted, nil
+
+}
+
+func sharedSecret(sk ed25519.PrivateKey, pk ed25519.PublicKey) ([]byte, error) {
+	private := PrivateKeyToCurve25519(sk)
+	public := PublicKeyToCurve25519(pk)
+
+	shareSecret, err := curve25519.X25519(private[:], public[:])
+	if err != nil {
+		return nil, err
+	}
+	return shareSecret, nil
+}
diff --git a/pkg/identity.go b/pkg/identity.go
index c759c054..71d86287 100644
--- a/pkg/identity.go
+++ b/pkg/identity.go
@@ -39,6 +39,13 @@ type IdentityManager interface {
 	// Decrypt decrypts message with the private of the node
 	Decrypt(message []byte) ([]byte, error)
 
+	// EncryptECDH aes encrypt msg using a shared key derived from private key of the node and public key of the other party using Elliptic curve Diffie Helman algorithm
+	// the nonce if prepended to the encrypted message
+	EncryptECDH(msg []byte, publicKey []byte) ([]byte, error)
+
+	// DecryptECDH decrypt aes encrypted msg using a shared key derived from private key of the node and public key of the other party using Elliptic curve Diffie Helman algorithm
+	DecryptECDH(msg []byte, publicKey []byte) ([]byte, error)
+
 	// PrivateKey sends the keypair
 	PrivateKey() []byte
 }
diff --git a/pkg/identity/identityd.go b/pkg/identity/identityd.go
index 16c2c10b..164a91de 100644
--- a/pkg/identity/identityd.go
+++ b/pkg/identity/identityd.go
@@ -74,6 +74,17 @@ func (d *identityManager) Decrypt(message []byte) ([]byte, error) {
 	return crypto.Decrypt(message, d.key.PrivateKey)
 }
 
+// EncryptECDH encrypt msg using AES with shared key derived from private key of the node and public key of the other party using Elliptic curve Diffie Helman algorithm
+// the nonce if prepended to the encrypted message
+func (d *identityManager) EncryptECDH(msg []byte, pk []byte) ([]byte, error) {
+	return crypto.EncryptECDH(msg, d.key.PrivateKey, pk)
+}
+
+// DecryptECDH decrypt AES encrypted msg using a shared key derived from private key of the node and public key of the other party using Elliptic curve Diffie Helman algorithm
+func (d *identityManager) DecryptECDH(msg []byte, pk []byte) ([]byte, error) {
+	return crypto.DecryptECDH(msg, d.key.PrivateKey, pk)
+}
+
 // PrivateKey returns the private key of the node
 func (d *identityManager) PrivateKey() []byte {
 	return d.key.PrivateKey
diff --git a/pkg/provision/primitives/container.go b/pkg/provision/primitives/container.go
index 9b5ff5c3..afe16f21 100644
--- a/pkg/provision/primitives/container.go
+++ b/pkg/provision/primitives/container.go
@@ -169,7 +169,7 @@ func (p *Provisioner) containerProvisionImpl(ctx context.Context, reservation *p
 	}
 
 	for k, v := range config.SecretEnv {
-		v, err := decryptSecret(p.zbus, v)
+		v, err := decryptSecret(v, reservation.User, reservation.Version, p.zbus)
 		if err != nil {
 			return ContainerResult{}, errors.Wrapf(err, "failed to decrypt secret env var '%s'", k)
 		}
@@ -182,14 +182,14 @@ func (p *Provisioner) containerProvisionImpl(ctx context.Context, reservation *p
 		stderr := log.Data.Stderr
 
 		if len(log.Data.SecretStdout) > 0 {
-			stdout, err = decryptSecret(p.zbus, log.Data.SecretStdout)
+			stdout, err = decryptSecret(log.Data.SecretStdout, reservation.User, reservation.Version, p.zbus)
 			if err != nil {
 				return ContainerResult{}, errors.Wrap(err, "failed to decrypt log.secret_stdout var")
 			}
 		}
 
 		if len(log.Data.SecretStderr) > 0 {
-			stderr, err = decryptSecret(p.zbus, log.Data.SecretStderr)
+			stderr, err = decryptSecret(log.Data.SecretStderr, reservation.User, reservation.Version, p.zbus)
 			if err != nil {
 				return ContainerResult{}, errors.Wrap(err, "failed to decrypt log.secret_stdout var")
 			}
diff --git a/pkg/provision/primitives/converter.go b/pkg/provision/primitives/converter.go
index 285f0fc3..b4c9d082 100644
--- a/pkg/provision/primitives/converter.go
+++ b/pkg/provision/primitives/converter.go
@@ -258,6 +258,7 @@ func WorkloadToProvisionType(w workloads.Workloader) (*provision.Reservation, er
 		ToDelete:  w.GetNextAction() == workloads.NextActionDelete,
 		Reference: w.GetReference(),
 		Result:    resultFromSchemaType(w.GetResult()),
+		Version:   w.GetVersion(),
 	}
 
 	var (
diff --git a/pkg/provision/primitives/crypto.go b/pkg/provision/primitives/crypto.go
index faa76cc1..87aa4a27 100644
--- a/pkg/provision/primitives/crypto.go
+++ b/pkg/provision/primitives/crypto.go
@@ -1,16 +1,22 @@
 package primitives
 
 import (
+	"crypto/ed25519"
 	"encoding/hex"
+	"fmt"
+	"strconv"
 
+	"github.com/threefoldtech/tfexplorer/schema"
 	"github.com/threefoldtech/zbus"
+	"github.com/threefoldtech/zos/pkg/app"
 	"github.com/threefoldtech/zos/pkg/stubs"
 )
 
-func decryptSecret(client zbus.Client, secret string) (string, error) {
+func decryptSecret(secret, userID string, reservationVersion int, client zbus.Client) (string, error) {
 	if len(secret) == 0 {
 		return "", nil
 	}
+
 	identity := stubs.NewIdentityManagerStub(client)
 
 	bytes, err := hex.DecodeString(secret)
@@ -18,6 +24,44 @@ func decryptSecret(client zbus.Client, secret string) (string, error) {
 		return "", err
 	}
 
-	out, err := identity.Decrypt(bytes)
+	var (
+		out        []byte
+		userPubKey ed25519.PublicKey
+	)
+	switch reservationVersion {
+	case 0:
+		out, err = identity.Decrypt(bytes)
+	case 1:
+		userPubKey, err = fetchUserPublicKey(userID)
+		if err != nil {
+			return "", fmt.Errorf("failed to retrieve user %s public key: %w", userID, err)
+		}
+		out, err = identity.DecryptECDH(bytes, userPubKey)
+	}
+
 	return string(out), err
 }
+
+func fetchUserPublicKey(userID string) (ed25519.PublicKey, error) {
+	iid, err := strconv.Atoi(userID)
+	if err != nil {
+		return nil, err
+	}
+
+	explorer, err := app.ExplorerClient()
+	if err != nil {
+		return nil, err
+	}
+
+	user, err := explorer.Phonebook.Get(schema.ID(iid))
+	if err != nil {
+		return nil, err
+	}
+
+	b, err := hex.DecodeString(user.Pubkey)
+	if err != nil {
+		return nil, err
+	}
+
+	return ed25519.PublicKey(b), nil
+}
diff --git a/pkg/provision/primitives/kubernetes.go b/pkg/provision/primitives/kubernetes.go
index e58e2382..09b46cf8 100644
--- a/pkg/provision/primitives/kubernetes.go
+++ b/pkg/provision/primitives/kubernetes.go
@@ -85,7 +85,7 @@ func (p *Provisioner) kubernetesProvisionImpl(ctx context.Context, reservation *
 	result.ID = reservation.ID
 	result.IP = config.IP.String()
 
-	config.PlainClusterSecret, err = decryptSecret(p.zbus, config.ClusterSecret)
+	config.PlainClusterSecret, err = decryptSecret(config.ClusterSecret, reservation.User, reservation.Version, p.zbus)
 	if err != nil {
 		return result, errors.Wrap(err, "failed to decrypt namespace password")
 	}
diff --git a/pkg/provision/primitives/zdb.go b/pkg/provision/primitives/zdb.go
index e5939aed..d5ae6e5e 100644
--- a/pkg/provision/primitives/zdb.go
+++ b/pkg/provision/primitives/zdb.go
@@ -66,7 +66,7 @@ func (p *Provisioner) zdbProvisionImpl(ctx context.Context, reservation *provisi
 	}
 
 	var err error
-	config.PlainPassword, err = decryptSecret(p.zbus, config.Password)
+	config.PlainPassword, err = decryptSecret(config.Password, reservation.User, reservation.Version, p.zbus)
 	if err != nil {
 		return ZDBResult{}, errors.Wrap(err, "failed to decrypt namespace password")
 	}
diff --git a/pkg/provision/reservation.go b/pkg/provision/reservation.go
index 25600c0b..a76e874e 100644
--- a/pkg/provision/reservation.go
+++ b/pkg/provision/reservation.go
@@ -48,6 +48,8 @@ type Reservation struct {
 	Reference string `json:"reference"`
 
 	Result Result `json:"result"`
+
+	Version int `json:"version"`
 }
 
 // AppendTag appends tags
diff --git a/pkg/stubs/identity_stub.go b/pkg/stubs/identity_stub.go
index 072b1a93..20331161 100644
--- a/pkg/stubs/identity_stub.go
+++ b/pkg/stubs/identity_stub.go
@@ -38,6 +38,22 @@ func (s *IdentityManagerStub) Decrypt(arg0 []uint8) (ret0 []uint8, ret1 error) {
 	return
 }
 
+func (s *IdentityManagerStub) DecryptECDH(arg0 []uint8, arg1 []uint8) (ret0 []uint8, ret1 error) {
+	args := []interface{}{arg0, arg1}
+	result, err := s.client.Request(s.module, s.object, "DecryptECDH", args...)
+	if err != nil {
+		panic(err)
+	}
+	if err := result.Unmarshal(0, &ret0); err != nil {
+		panic(err)
+	}
+	ret1 = new(zbus.RemoteError)
+	if err := result.Unmarshal(1, &ret1); err != nil {
+		panic(err)
+	}
+	return
+}
+
 func (s *IdentityManagerStub) Encrypt(arg0 []uint8) (ret0 []uint8, ret1 error) {
 	args := []interface{}{arg0}
 	result, err := s.client.Request(s.module, s.object, "Encrypt", args...)
@@ -54,6 +70,22 @@ func (s *IdentityManagerStub) Encrypt(arg0 []uint8) (ret0 []uint8, ret1 error) {
 	return
 }
 
+func (s *IdentityManagerStub) EncryptECDH(arg0 []uint8, arg1 []uint8) (ret0 []uint8, ret1 error) {
+	args := []interface{}{arg0, arg1}
+	result, err := s.client.Request(s.module, s.object, "EncryptECDH", args...)
+	if err != nil {
+		panic(err)
+	}
+	if err := result.Unmarshal(0, &ret0); err != nil {
+		panic(err)
+	}
+	ret1 = new(zbus.RemoteError)
+	if err := result.Unmarshal(1, &ret1); err != nil {
+		panic(err)
+	}
+	return
+}
+
 func (s *IdentityManagerStub) FarmID() (ret0 pkg.FarmID, ret1 error) {
 	args := []interface{}{}
 	result, err := s.client.Request(s.module, s.object, "FarmID", args...)


Test patch:
diff --git a/pkg/crypto/crypto_test.go b/pkg/crypto/crypto_test.go
index 4e28162b..fc8ee0d3 100644
--- a/pkg/crypto/crypto_test.go
+++ b/pkg/crypto/crypto_test.go
@@ -2,6 +2,7 @@ package crypto
 
 import (
 	"crypto/rand"
+	"encoding/hex"
 	"fmt"
 	"testing"
 
@@ -87,3 +88,44 @@ func TestPyNACLCompatibilityEncryption(t *testing.T) {
 	require.NoError(t, err)
 	fmt.Printf("%x\n", x)
 }
+
+func TestECDH(t *testing.T) {
+	alicePubkey, alicePrivkey, err := ed25519.GenerateKey(nil)
+	require.NoError(t, err)
+
+	bobPubkey, bobPrivkey, err := ed25519.GenerateKey(nil)
+	require.NoError(t, err)
+
+	msg := []byte("hello world")
+	encrypted, err := EncryptECDH(msg, alicePrivkey, bobPubkey)
+	require.NoError(t, err)
+
+	msg2, err := DecryptECDH(encrypted, bobPrivkey, alicePubkey)
+	require.NoError(t, err)
+
+	assert.Equal(t, msg, msg2)
+}
+
+func TestECDHPyNACLCompatibility(t *testing.T) {
+	// import nacl.public
+	// import nacl.encoding
+	// from nacl.secret import SecretBox
+	// from nacl.bindings import crypto_scalarmult
+	// alice_private = nacl.public.PrivateKey.from_seed(b"11111111111111111111111111111111")
+	// bob_private = nacl.public.PrivateKey.from_seed(b"22222222222222222222222222222222")
+	// shared_secret = crypto_scalarmult(alice_private.encode(), bob_private.public_key.encode())
+	// box = SecretBox(shared_secret)
+	// encrypted = box.encrypt(b'hello world')
+	// print(nacl.encoding.HexEncoder().encode(encrypted))
+	// b'74bb3109ad0a1947473ba6bccd3f44a8d735d6a99f8d046dff6e3853b664ad09148a2bf427a95d502c8222b62e4fc8603b2407'
+
+	alicePrivate := ed25519.NewKeyFromSeed([]byte("11111111111111111111111111111111"))
+	bobPrivate := ed25519.NewKeyFromSeed([]byte("22222222222222222222222222222222"))
+
+	encrypted, err := hex.DecodeString("74bb3109ad0a1947473ba6bccd3f44a8d735d6a99f8d046dff6e3853b664ad09148a2bf427a95d502c8222b62e4fc8603b2407")
+	require.NoError(t, err)
+
+	decrypted, err := DecryptECDH(encrypted, bobPrivate, alicePrivate.Public().(ed25519.PublicKey))
+	require.NoError(t, err)
+	assert.Equal(t, []byte("hello world"), decrypted)
+}
diff --git a/pkg/network/nr/net_resource_test.go b/pkg/network/nr/net_resource_test.go
index 1c618d10..a96ef42c 100644
--- a/pkg/network/nr/net_resource_test.go
+++ b/pkg/network/nr/net_resource_test.go
@@ -50,6 +50,17 @@ func (t *testIdentityManager) Decrypt(message []byte) ([]byte, error) {
 	return nil, fmt.Errorf("not implemented")
 }
 
+// EncryptECDH aes encrypt msg using a shared key derived from private key of the node and public key of the other party using Elliptic curve Diffie Helman algorithm
+// the nonce if prepended to the encrypted message
+func (t *testIdentityManager) EncryptECDH(msg []byte, publicKey []byte) ([]byte, error) {
+	return nil, fmt.Errorf("not implemented")
+}
+
+// DecryptECDH decrypt aes encrypted msg using a shared key derived from private key of the node and public key of the other party using Elliptic curve Diffie Helman algorithm
+func (t *testIdentityManager) DecryptECDH(msg []byte, publicKey []byte) ([]byte, error) {
+	return nil, fmt.Errorf("not implemented")
+}
+
 // PrivateKey sends the keypair
 func (t *testIdentityManager) PrivateKey() []byte {
 	return nil