Repository: bitmaelum/bitmaelum-suite
Pull request #147 (2021-01-08 20:04:08)

Key rotate


Patch:
diff --git a/buildscripts/docker/Dockerfile.suite b/buildscripts/docker/Dockerfile.suite
new file mode 100644
index 0000000..a3534d6
--- /dev/null
+++ b/buildscripts/docker/Dockerfile.suite
@@ -0,0 +1,10 @@
+FROM debian:buster-slim
+MAINTAINER jthijssen@bitmaelum.com
+
+# We need CA certificates otherwise we cannot connect to https://
+RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates netbase && rm -rf /var/lib/apt/lists/*
+
+COPY bm-* /usr/local/bin/
+
+RUN mkdir /etc/bitmaelum && ln -sf /bitmaelum/server-config.yml /etc/bitmaelum/bitmaelum-server-config.yml
+COPY docker-entrypoint-suite.sh /usr/local/bin/docker-entrypoint.sh
diff --git a/cmd/bm-client/cmd/account_key.go b/cmd/bm-client/cmd/account_key.go
new file mode 100644
index 0000000..d16bc65
--- /dev/null
+++ b/cmd/bm-client/cmd/account_key.go
@@ -0,0 +1,41 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+var accountKeyCmd = &cobra.Command{
+	Use:   "key",
+	Short: "Account key management",
+}
+
+var (
+	akAccount *string
+)
+
+func init() {
+	accountCmd.AddCommand(accountKeyCmd)
+
+	akAccount = accountKeyCmd.PersistentFlags().String("account", "", "Account to key on")
+
+	_ = accountKeyCmd.MarkPersistentFlagRequired("account")
+}
diff --git a/cmd/bm-client/cmd/account_key_advertise.go b/cmd/bm-client/cmd/account_key_advertise.go
new file mode 100644
index 0000000..b9fda47
--- /dev/null
+++ b/cmd/bm-client/cmd/account_key_advertise.go
@@ -0,0 +1,76 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/bitmaelum/bitmaelum-suite/cmd/bm-client/internal/container"
+	"github.com/bitmaelum/bitmaelum-suite/internal/vault"
+	"github.com/spf13/cobra"
+)
+
+var accountKeyAdvertiseCmd = &cobra.Command{
+	Use:   "advertise",
+	Short: "Advertise new key to the key resolver",
+	Run: func(cmd *cobra.Command, args []string) {
+		v := vault.OpenDefaultVault()
+
+		info, err := vault.GetAccount(v, *akAccount)
+		if err != nil {
+			fmt.Println("cannot find account in vault")
+			os.Exit(1)
+		}
+
+		vkp, err := info.FindKey(*akaKey)
+		if err != nil {
+			fmt.Println("cannot find key in account")
+			os.Exit(1)
+		}
+
+		info.SetActiveKey(&vkp.KeyPair)
+
+		// Save vault
+		err = v.Persist()
+		if err != nil {
+			fmt.Println("cannot save vault")
+			os.Exit(1)
+		}
+
+		ks := container.Instance.GetResolveService()
+		err = ks.UploadAddressInfo(*info, "")
+		if err != nil {
+			fmt.Println("cannot advertise key")
+			os.Exit(1)
+		}
+
+	},
+}
+
+var akaKey *string
+
+func init() {
+	accountKeyCmd.AddCommand(accountKeyAdvertiseCmd)
+
+	akaKey = accountKeyAdvertiseCmd.Flags().StringP("key", "k", "", "fingerprint of the key to advertise")
+
+	_ = accountKeyAdvertiseCmd.MarkPersistentFlagRequired("account")
+}
diff --git a/cmd/bm-client/cmd/account_key_generate.go b/cmd/bm-client/cmd/account_key_generate.go
new file mode 100644
index 0000000..cb97020
--- /dev/null
+++ b/cmd/bm-client/cmd/account_key_generate.go
@@ -0,0 +1,120 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/bitmaelum/bitmaelum-suite/cmd/bm-client/internal/container"
+	"github.com/bitmaelum/bitmaelum-suite/internal"
+	"github.com/bitmaelum/bitmaelum-suite/internal/vault"
+	"github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+var accountKeyGenerateCmd = &cobra.Command{
+	Use:   "generate",
+	Short: "Generate a new account key",
+	Long: `Generate a new key into your account. The keytype (--keytype, -k) can be either one of the following:
+	
+  ed25519   ED25519 elliptic curve
+  rsa       RSA 2048 bit 
+  rsa4096   RSA 4096 bit 
+
+`, Run: func(cmd *cobra.Command, args []string) {
+		v := vault.OpenDefaultVault()
+
+		info, err := vault.GetAccount(v, *akAccount)
+		if err != nil {
+			fmt.Println("cannot find account in vault")
+			os.Exit(1)
+		}
+
+		kt, err := bmcrypto.FindKeyType(*keyType)
+		if err != nil {
+			logrus.Fatal(err)
+		}
+
+		kp, err := internal.GenerateKeypairWithRandomSeed(kt)
+		if err != nil {
+			logrus.Fatal(err)
+		}
+
+		info.Keys = append(info.Keys, vault.KeyPair{
+			KeyPair: *kp,
+			Active:  false,
+		})
+
+		fmt.Print(`
+*****************************************************************************
+!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORT
+*****************************************************************************
+
+We have generated a private key which allows you to control your account. 
+If, for any reason, you lose this key, you will need to use the following 
+words in order to recreate the key:
+
+`)
+		fmt.Print(internal.WordWrap(internal.GetMnemonic(kp), 78))
+		fmt.Print(`
+
+Write these words down and store them in a secure environment. They are the 
+ONLY way to recover your private key in case you lose it.
+
+WITHOUT THESE WORDS, ALL ACCESS TO YOUR ACCOUNT IS LOST!
+
+
+`)
+
+		// Upload the new key to the server
+		if *advertise {
+			info.SetActiveKey(kp)
+
+			ks := container.Instance.GetResolveService()
+			err = ks.UploadAddressInfo(*info, "")
+		}
+
+		// Error or we didn't advertise
+		if err != nil || !*advertise {
+			fmt.Printf("Note: your key is not yet advertised. To make sure others use your key, please run:\n\n   bm-client account key advertise --account %s --key %s`\n", info.Address, kp.FingerPrint)
+		}
+
+		// Save vault
+		err = v.Persist()
+		if err != nil {
+			fmt.Println("cannot save vault")
+			os.Exit(1)
+		}
+	},
+}
+
+var (
+	keyType   *string
+	advertise *bool
+)
+
+func init() {
+	accountKeyCmd.AddCommand(accountKeyGenerateCmd)
+
+	keyType = accountKeyGenerateCmd.Flags().StringP("keytype", "k", "ed25519", "The key type (defaults to ed25519)")
+	advertise = accountKeyGenerateCmd.Flags().BoolP("advertise", "a", false, "Automatically advertise the key")
+}
diff --git a/cmd/bm-client/cmd/account_key_list.go b/cmd/bm-client/cmd/account_key_list.go
new file mode 100644
index 0000000..174621f
--- /dev/null
+++ b/cmd/bm-client/cmd/account_key_list.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/bitmaelum/bitmaelum-suite/internal"
+	"github.com/bitmaelum/bitmaelum-suite/internal/vault"
+	"github.com/olekukonko/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+var accountKeyListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Display account keys",
+	Run: func(cmd *cobra.Command, args []string) {
+		v := vault.OpenDefaultVault()
+
+		info, err := vault.GetAccount(v, *akAccount)
+		if err != nil {
+			fmt.Println("cannot find account in vault")
+			os.Exit(1)
+		}
+
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"Created at", "Type", "Active", "Fingerprint"})
+
+		for _, key := range info.Keys {
+			active := ""
+			if key.Active {
+				active = "*"
+			}
+
+			table.Append([]string{
+				internal.TimeNow().String(),
+				key.PubKey.Type.String(),
+				active,
+				key.FingerPrint,
+			})
+		}
+
+		table.Render()
+	},
+}
+
+func init() {
+	accountKeyCmd.AddCommand(accountKeyListCmd)
+}
diff --git a/cmd/bm-client/handlers/create_account.go b/cmd/bm-client/handlers/create_account.go
index 11a2c98..e256f4c 100644
--- a/cmd/bm-client/handlers/create_account.go
+++ b/cmd/bm-client/handlers/create_account.go
@@ -108,8 +108,6 @@ func checkAccountInVault(vault *vault.Vault, addr address.Address) *vault.Accoun
 
 // CreateAccount creates a new account locally in the vault, stores it on the mail server and pushes the public key to the resolver
 func CreateAccount(v *vault.Vault, bmAddr, name, token string, kt bmcrypto.KeyType) {
-	var mnemonicToShow string
-
 	fmt.Println("")
 
 	addr := verifyAddress(bmAddr)
@@ -120,17 +118,20 @@ func CreateAccount(v *vault.Vault, bmAddr, name, token string, kt bmcrypto.KeyTy
 	res := container.Instance.GetResolveService()
 	resolverCfg := res.GetConfig()
 
+	var (
+		err error
+		kp  *bmcrypto.KeyPair
+	)
+
 	if info == nil {
 		fmt.Printf("* Generating your secret key to send and read mail: ")
 
-		mnemonic, e, privKey, pubKey, err := bminternal.GenerateKeypairWithMnemonic(kt)
-
+		kp, err = bminternal.GenerateKeypairWithRandomSeed(kt)
 		if err != nil {
 			fmt.Print(err)
 			fmt.Println("")
 			os.Exit(1)
 		}
-		mnemonicToShow = mnemonic
 		fmt.Printf("done.\n")
 
 		fmt.Printf("* Doing some work to let people know this is not a fake account, this might take a while: ")
@@ -144,11 +145,8 @@ func CreateAccount(v *vault.Vault, bmAddr, name, token string, kt bmcrypto.KeyTy
 			Name:    name,
 			Keys: []vault.KeyPair{
 				{
-					Generator:   e,
-					FingerPrint: pubKey.Fingerprint(),
-					PrivKey:     *privKey,
-					PubKey:      *pubKey,
-					Active:      true,
+					KeyPair: *kp,
+					Active:  true,
 				},
 			},
 			Pow:       proof,
@@ -212,24 +210,24 @@ func CreateAccount(v *vault.Vault, bmAddr, name, token string, kt bmcrypto.KeyTy
 	fmt.Printf("\n")
 	fmt.Printf("* All done")
 
-	if len(mnemonicToShow) > 0 {
+	if kp != nil {
 		fmt.Print(`
-*****************************************************************************
-!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORT
-*****************************************************************************
-
-We have generated a private key which allows you to control your account. 
-If, for any reason, you lose this key, you will need to use the following 
-words in order to recreate the key:
-
-`)
-		fmt.Print(bminternal.WordWrap(mnemonicToShow, 78))
+	*****************************************************************************
+	!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORT
+	*****************************************************************************
+	
+	We have generated a private key which allows you to control your account. 
+	If, for any reason, you lose this key, you will need to use the following 
+	words in order to recreate the key:
+	
+	`)
+		fmt.Print(bminternal.WordWrap(bminternal.GetMnemonic(kp), 78))
 		fmt.Print(`
-
-Write these words down and store them in a secure environment. They are the 
-ONLY way to recover your private key in case you lose it.
-
-WITHOUT THESE WORDS, ALL ACCESS TO YOUR ACCOUNT IS LOST!
-`)
+	
+	Write these words down and store them in a secure environment. They are the 
+	ONLY way to recover your private key in case you lose it.
+	
+	WITHOUT THESE WORDS, ALL ACCESS TO YOUR ACCOUNT IS LOST!
+	`)
 	}
 }
diff --git a/cmd/bm-client/handlers/create_organisation.go b/cmd/bm-client/handlers/create_organisation.go
index ea76515..d9eef1f 100644
--- a/cmd/bm-client/handlers/create_organisation.go
+++ b/cmd/bm-client/handlers/create_organisation.go
@@ -56,7 +56,9 @@ func CreateOrganisation(v *vault.Vault, orgAddr, fullName string, orgValidations
 	}
 	fmt.Printf("not found. This is a good thing.\n")
 
-	var mnemonic string
+	var (
+		kp *bmcrypto.KeyPair
+	)
 
 	fmt.Printf("* Checking if the organisation is already present in the vault: ")
 	var info *vault.OrganisationInfo
@@ -69,15 +71,7 @@ func CreateOrganisation(v *vault.Vault, orgAddr, fullName string, orgValidations
 
 		fmt.Printf("* Generating organisation public/private key pair: ")
 
-		var (
-			privKey *bmcrypto.PrivKey
-			pubKey  *bmcrypto.PubKey
-			err     error
-			e       string
-		)
-
-		mnemonic, e, privKey, pubKey, err = internal.GenerateKeypairWithMnemonic(kt)
-
+		kp, err = internal.GenerateKeypairWithRandomSeed(kt)
 		if err != nil {
 			fmt.Print(err)
 			fmt.Println("")
@@ -98,11 +92,8 @@ func CreateOrganisation(v *vault.Vault, orgAddr, fullName string, orgValidations
 			FullName: fullName,
 			Keys: []vault.KeyPair{
 				{
-					Generator:   e,
-					FingerPrint: pubKey.Fingerprint(),
-					PrivKey:     *privKey,
-					PubKey:      *pubKey,
-					Active:      true,
+					KeyPair: *kp,
+					Active:  true,
 				},
 			},
 			Pow:         proof,
@@ -134,8 +125,7 @@ func CreateOrganisation(v *vault.Vault, orgAddr, fullName string, orgValidations
 	fmt.Printf("\n")
 	fmt.Printf("* All done")
 
-	if len(mnemonic) > 0 {
-		fmt.Print(`
+	fmt.Print(`
 *****************************************************************************
 !IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORT
 *****************************************************************************
@@ -145,13 +135,12 @@ If, for any reason, you lose this key, you will need to use the following
 words in order to recreate the key:
 
 `)
-		fmt.Print(internal.WordWrap(mnemonic, 78))
-		fmt.Print(`
+	fmt.Print(internal.WordWrap(internal.GetMnemonic(kp), 78))
+	fmt.Print(`
 
 Write these words down and store them in a secure environment. They are the 
 ONLY way to recover your private key in case you lose it.
 
 WITHOUT THESE WORDS, ALL ACCESS TO YOUR ORGANISATION IS LOST!
 `)
-	}
 }
diff --git a/cmd/bm-client/handlers/read_message.go b/cmd/bm-client/handlers/read_message.go
index 88b6296..bea8410 100644
--- a/cmd/bm-client/handlers/read_message.go
+++ b/cmd/bm-client/handlers/read_message.go
@@ -61,11 +61,29 @@ func ReadMessages(info *vault.AccountInfo, routingInfo *resolver.RoutingInfo, bo
 
 	// iterate list until we quit
 	for !readDone {
-		decryptedMsg, err := messageList[idx].Decrypt(info.GetActiveKey().PrivKey)
-		if err != nil {
-			fmt.Println("Cannot decrypt message: ", err)
-		} else {
+		// Find key, or iterate all keys when key is not found
+		keys := info.Keys
+		key, err := info.FindKey(messageList[idx].Header.To.Fingerprint)
+		if err == nil {
+			keys = []vault.KeyPair{*key}
+		}
+
+		// Iterate all keys (or the single key), and see if we can decrypt the message
+		var decryptedMsg *message.DecryptedMessage
+		for _, key := range keys {
+			var err error
+			decryptedMsg, err = messageList[idx].Decrypt(key.PrivKey)
+			if err == nil {
+				break
+			}
+			decryptedMsg = nil
+		}
+
+		// Display message or fail when not decrypted
+		if decryptedMsg != nil {
 			displayMessage(*decryptedMsg)
+		} else {
+			fmt.Println("Cannot decrypt message: ", err)
 		}
 
 		parseDone := false
diff --git a/cmd/bm-config/cmd/generate_routing_id.go b/cmd/bm-config/cmd/generate_routing_id.go
index be8fb76..737c028 100644
--- a/cmd/bm-config/cmd/generate_routing_id.go
+++ b/cmd/bm-config/cmd/generate_routing_id.go
@@ -20,6 +20,7 @@
 package cmd
 
 import (
+	"encoding/hex"
 	"fmt"
 	"os"
 
@@ -40,36 +41,42 @@ the server on the network.
 This command creates a new routing file if one does not exist.`,
 	Run: func(cmd *cobra.Command, args []string) {
 		if config.Server.Server.RoutingFile == "" {
-			logrus.Fatalf("Routing file path is not found in your server configuration.")
+			fmt.Println("Routing file path is not found in your server configuration.")
+			os.Exit(1)
 		}
 
 		// Check if file exist
 		_, err := os.Stat(config.Server.Server.RoutingFile)
 		if os.IsExist(err) {
-			logrus.Fatalf("Routing file %s already exist. I will not overwrite this file.", config.Server.Server.RoutingFile)
+			fmt.Printf("Routing file %s already exist. I will not overwrite this file.", config.Server.Server.RoutingFile)
+			os.Exit(1)
 		}
 
 		var (
 			mnemonic string
 			r        *config.RoutingConfig
 		)
+
 		if ok, _ := cmd.Flags().GetBool("mnemonic"); ok {
 			// ask for mnemonic
 			mnemonic = console.AskMnemonicPhrase()
 			r, err = config.GenerateRoutingFromMnemonic(mnemonic)
+			checkError(err)
 		} else {
 			// Generate new routing
-			mnemonic, r, err = config.GenerateRouting()
-		}
-		if err != nil {
-			logrus.Fatalf("Error while generating routing file: %v", err)
+			r, err = config.GenerateRouting()
+			checkError(err)
+
+			seed, err := hex.DecodeString(r.KeyPair.Generator)
+			checkError(err)
+
+			mnemonic, err = internal.RandomSeedToMnemonic(seed)
+			checkError(err)
 		}
 
 		// Save routing
 		err = config.SaveRouting(config.Server.Server.RoutingFile, r)
-		if err != nil {
-			logrus.Fatalf("Error while creating routing file: %v", err)
-		}
+		checkError(err)
 
 		logrus.Printf("Generated routing file: %s", config.Server.Server.RoutingFile)
 
@@ -92,6 +99,13 @@ ONLY way to recover your private key in case you lose it.
 	},
 }
 
+func checkError(err error) {
+	if err != nil {
+		fmt.Printf("Error while generating routing file: %s\n", err)
+		os.Exit(1)
+	}
+}
+
 func init() {
 	rootCmd.AddCommand(initRoutingConfigCmd)
 
diff --git a/cmd/bm-config/cmd/invite.go b/cmd/bm-config/cmd/invite.go
index 8ddf52a..a58e696 100644
--- a/cmd/bm-config/cmd/invite.go
+++ b/cmd/bm-config/cmd/invite.go
@@ -56,7 +56,7 @@ server. Only the specified address can register the account`,
 		}
 
 		validUntil := internal.TimeNow().Add(duration)
-		token, err := signature.NewInviteToken(addr.Hash(), config.Routing.RoutingID, validUntil, config.Routing.PrivateKey)
+		token, err := signature.NewInviteToken(addr.Hash(), config.Routing.RoutingID, validUntil, config.Routing.KeyPair.PrivKey)
 		if err != nil {
 			msg := fmt.Sprintf("error while inviting address: %s", err)
 			outError(msg, asJSON)
diff --git a/cmd/bm-server/handler/account.go b/cmd/bm-server/handler/account.go
index 5f14c42..27d2197 100644
--- a/cmd/bm-server/handler/account.go
+++ b/cmd/bm-server/handler/account.go
@@ -84,7 +84,7 @@ func CreateAccount(w http.ResponseWriter, req *http.Request) {
 	}
 
 	// Check if we need to verify against the mail server key, or the organisation key
-	var pubKey = config.Routing.PublicKey
+	var pubKey = config.Routing.KeyPair.PubKey
 	if !orgHash.IsEmpty() {
 		r := container.Instance.GetResolveService()
 		oh, err := hash.NewFromHash(input.OrgHash)
diff --git a/cmd/bm-server/handler/mgmt/invite.go b/cmd/bm-server/handler/mgmt/invite.go
index 107f7ec..5896fe9 100644
--- a/cmd/bm-server/handler/mgmt/invite.go
+++ b/cmd/bm-server/handler/mgmt/invite.go
@@ -60,7 +60,7 @@ func NewInvite(w http.ResponseWriter, req *http.Request) {
 	}
 
 	validUntil := internal.TimeNow().Add(time.Duration(input.Days) * 24 * time.Hour)
-	token, err := signature.NewInviteToken(*addrHash, config.Routing.RoutingID, validUntil, config.Routing.PrivateKey)
+	token, err := signature.NewInviteToken(*addrHash, config.Routing.RoutingID, validUntil, config.Routing.KeyPair.PrivKey)
 	if err != nil {
 		httputils.ErrorOut(w, http.StatusInternalServerError, "cannot generate invite token")
 		return
diff --git a/cmd/bm-server/main.go b/cmd/bm-server/main.go
index acefb72..58593d7 100644
--- a/cmd/bm-server/main.go
+++ b/cmd/bm-server/main.go
@@ -127,9 +127,9 @@ You can generate a new one by running:
 		// Upload routing
 		err := res.UploadRoutingInfo(resolver.RoutingInfo{
 			Hash:      config.Routing.RoutingID,
-			PublicKey: config.Routing.PublicKey,
+			PublicKey: config.Routing.KeyPair.PubKey,
 			Routing:   config.Server.Server.Hostname,
-		}, &config.Routing.PrivateKey)
+		}, &config.Routing.KeyPair.PrivKey)
 		if err != nil {
 			fmt.Print("There is an error while uploading routing information to the key resolver: ", err)
 			os.Exit(1)
diff --git a/cmd/bm-server/processor/retry_queue.go b/cmd/bm-server/processor/retry_queue.go
index 195b3dc..84e2d0d 100644
--- a/cmd/bm-server/processor/retry_queue.go
+++ b/cmd/bm-server/processor/retry_queue.go
@@ -154,7 +154,7 @@ func sendPostmasterMail(toHash hash.Hash, subject string, body string) error {
 	senderName := fmt.Sprintf("postmaster at %s", config.Server.Server.Hostname)
 
 	addressing := message.NewAddressing(message.SignedByTypeServer)
-	addressing.AddSender(nil, fromHash, senderName, config.Routing.PrivateKey, "host")
+	addressing.AddSender(nil, fromHash, senderName, config.Routing.KeyPair.PrivKey, "host")
 	addressing.AddRecipient(nil, &toHash, &addr.PublicKey)
 
 	// Create a single block with our body
diff --git a/internal/config/routing.go b/internal/config/routing.go
index e07e7c6..b8ccb76 100644
--- a/internal/config/routing.go
+++ b/internal/config/routing.go
@@ -34,9 +34,15 @@ import (
 
 // RoutingConfig holds routing configuration for the mail server
 type RoutingConfig struct {
-	RoutingID  string           `json:"routing_id"`
-	PrivateKey bmcrypto.PrivKey `json:"private_key"`
-	PublicKey  bmcrypto.PubKey  `json:"public_key"`
+	Version   int               `json:"version"`
+	RoutingID string            `json:"routing_id"`
+	KeyPair   *bmcrypto.KeyPair `json:"keypair,omitempty"`
+}
+
+type routingConfigV1 struct {
+	RoutingID  string            `json:"routing_id"`
+	PrivateKey *bmcrypto.PrivKey `json:"private_key,omitempty"`
+	PublicKey  *bmcrypto.PubKey  `json:"public_key,omitempty"`
 }
 
 // Routing keeps the routing ID and keys
@@ -54,13 +60,41 @@ func ReadRouting(p string) error {
 		return err
 	}
 
-	Routing = RoutingConfig{}
-	err = json.Unmarshal(data, &Routing)
+	err = readConfigLatest(p, data)
+	if err != nil {
+		err = readConfigV1(p, data)
+	}
+
+	return err
+}
+
+func readConfigV1(p string, data []byte) error {
+	tmp := routingConfigV1{}
+	err := json.Unmarshal(data, &tmp)
+	if err != nil {
+		return err
+	}
+
+	// Migrate to new structure
+	Routing.KeyPair = &bmcrypto.KeyPair{
+		Generator:   "",
+		FingerPrint: tmp.PublicKey.Fingerprint(),
+		PrivKey:     *tmp.PrivateKey,
+		PubKey:      *tmp.PublicKey,
+	}
+
+	return SaveRouting(p, &Routing)
+}
+
+func readConfigLatest(p string, data []byte) error {
+	tmp := routingConfigV1{}
+	err := json.Unmarshal(data, &tmp)
 	if err != nil {
 		return err
 	}
 
-	return nil
+	Routing = RoutingConfig{}
+	return json.Unmarshal(data, &Routing)
 }
 
 // SaveRouting will save the routing into a file. It will overwrite if exists
@@ -80,35 +114,35 @@ func SaveRouting(p string, routing *RoutingConfig) error {
 
 // GenerateRoutingFromMnemonic generates a new routing file from the given seed
 func GenerateRoutingFromMnemonic(mnemonic string) (*RoutingConfig, error) {
-	privKey, pubKey, err := internal.GenerateKeypairFromMnemonic(mnemonic)
+	kp, err := internal.GenerateKeypairFromMnemonic(mnemonic)
 	if err != nil {
 		return nil, err
 	}
 
-	id := hex.EncodeToString(pubKey.K.(ed25519.PublicKey))
+	id := hex.EncodeToString(kp.PubKey.K.(ed25519.PublicKey))
 	return &RoutingConfig{
-		RoutingID:  hash.New(id).String(),
-		PrivateKey: *privKey,
-		PublicKey:  *pubKey,
+		Version:   1,
+		RoutingID: hash.New(id).String(),
+		KeyPair:   kp,
 	}, nil
 }
 
 // GenerateRouting generates a new routing structure
-func GenerateRouting() (string, *RoutingConfig, error) {
+func GenerateRouting() (*RoutingConfig, error) {
 	kt, err := bmcrypto.FindKeyType("ed25519")
 	if err != nil {
-		return "", nil, err
+		return nil, err
 	}
 
-	mnemonic, _, privKey, pubKey, err := internal.GenerateKeypairWithMnemonic(kt)
+	kp, err := internal.GenerateKeypairWithRandomSeed(kt)
 	if err != nil {
-		return "", nil, err
+		return nil, err
 	}
 
-	id := hex.EncodeToString(pubKey.K.(ed25519.PublicKey))
-	return mnemonic, &RoutingConfig{
-		RoutingID:  hash.New(id).String(),
-		PrivateKey: *privKey,
-		PublicKey:  *pubKey,
+	id := hex.EncodeToString(kp.PubKey.K.(ed25519.PublicKey))
+	return &RoutingConfig{
+		Version:   1,
+		RoutingID: hash.New(id).String(),
+		KeyPair:   kp,
 	}, nil
 }
diff --git a/internal/message/compose.go b/internal/message/compose.go
index 53af184..e32dfb6 100644
--- a/internal/message/compose.go
+++ b/internal/message/compose.go
@@ -27,28 +27,24 @@ import (
 // Compose will create a new message and places it inside an envelope. This can be used for actual sending the message
 func Compose(addressing Addressing, subject string, b, a []string) (*Envelope, error) {
 	var (
-		senderHash      hash.Hash
-		recipientHash   hash.Hash
-		senderPrivKey   *bmcrypto.PrivKey
-		recipientPubKey *bmcrypto.PubKey
-		cat             *Catalog
+		senderHash    hash.Hash
+		recipientHash hash.Hash
+		senderPrivKey *bmcrypto.PrivKey
+		cat           *Catalog
 	)
 
+	recipientPubKey := addressing.Recipient.PubKey
+	senderPrivKey = addressing.Sender.PrivKey
+
 	switch addressing.Type {
 	case SignedByTypeOrigin:
-		senderPrivKey = addressing.Sender.PrivKey
-		recipientPubKey = addressing.Recipient.PubKey
 		cat = NewCatalog(addressing, subject)
 
 	case SignedByTypeServer:
-		senderPrivKey = addressing.Sender.PrivKey
-		recipientPubKey = addressing.Recipient.PubKey
 		cat = NewCatalog(addressing, subject)
 		cat.AddFlags("postmaster")
 
 	case SignedByTypeAuthorized:
-		senderPrivKey = addressing.Sender.PrivKey
-		recipientPubKey = addressing.Recipient.PubKey
 		cat = NewCatalog(addressing, subject)
 		cat.AddFlags("onbehalf")
 	}
@@ -82,7 +78,7 @@ func Compose(addressing Addressing, subject string, b, a []string) (*Envelope, e
 		recipientHash = *addressing.Recipient.Hash
 	}
 
-	header, err := generateHeader(senderHash, recipientHash, addressing.Type)
+	header, err := generateHeader(senderHash, recipientHash, addressing.Type, *recipientPubKey)
 	if err != nil {
 		return nil, err
 	}
@@ -102,13 +98,15 @@ func Compose(addressing Addressing, subject string, b, a []string) (*Envelope, e
 }
 
 // Generate a header file based on the info provided
-func generateHeader(sender, recipient hash.Hash, origin SignedByType) (*Header, error) {
+func generateHeader(sender, recipient hash.Hash, origin SignedByType, pubKey bmcrypto.PubKey) (*Header, error) {
 	header := &Header{}
 
 	header.From.Addr = sender
-	header.To.Addr = recipient
 	header.From.SignedBy = origin
 
+	header.To.Addr = recipient
+	header.To.Fingerprint = pubKey.Fingerprint()
+
 	return header, nil
 }
 
diff --git a/internal/message/header.go b/internal/message/header.go
index 953d996..7460f4c 100644
--- a/internal/message/header.go
+++ b/internal/message/header.go
@@ -49,7 +49,8 @@ type Header struct {
 
 	// Information on the recipient of the message
 	To struct {
-		Addr hash.Hash `json:"address"` // Address hash of the recipient
+		Addr        hash.Hash `json:"address"`               // Address hash of the recipient
+		Fingerprint string    `json:"fingerprint,omitempty"` // The fingerprint used for encrypting to this user
 	} `json:"to"`
 
 	// Information about the catalog of this message
diff --git a/internal/message/signature.go b/internal/message/signature.go
index 0fae6a7..16259ab 100644
--- a/internal/message/signature.go
+++ b/internal/message/signature.go
@@ -38,7 +38,7 @@ func SignServerHeader(header *Header) error {
 		return err
 	}
 
-	sig, err := bmcrypto.Sign(config.Routing.PrivateKey, h)
+	sig, err := bmcrypto.Sign(config.Routing.KeyPair.PrivKey, h)
 	if err != nil {
 		return err
 	}
diff --git a/internal/mnemonic.go b/internal/mnemonic.go
index 1569b95..860fdb7 100644
--- a/internal/mnemonic.go
+++ b/internal/mnemonic.go
@@ -20,7 +20,6 @@
 package internal
 
 import (
-	"bytes"
 	"encoding/hex"
 	"strings"
 
@@ -28,41 +27,55 @@ import (
 	"github.com/tyler-smith/go-bip39"
 )
 
-// GenerateKeypairWithMnemonic generates a mnemonic, and a RSA keypair that can be generated through the same mnemonic again.
-func GenerateKeypairWithMnemonic(kt bmcrypto.KeyType) (string, string, *bmcrypto.PrivKey, *bmcrypto.PubKey, error) {
-	// Generate large enough random string
-	e, err := bip39.NewEntropy(192)
+// GetMnemonic will return a mnemonic representation of the given keypair
+func GetMnemonic(kp *bmcrypto.KeyPair) string {
+	seed, err := hex.DecodeString(kp.Generator)
 	if err != nil {
-		return "", "", nil, nil, err
+		return ""
 	}
 
-	// Generate Mnemonic words
-	mnemonic, err := bip39.NewMnemonic(e)
+	mnemonic, err := RandomSeedToMnemonic(seed)
 	if err != nil {
-		return "", "", nil, nil, err
+		return ""
 	}
 
-	privKey, pubKey, err := kt.GenerateKeyPair(bytes.NewReader(e))
+	return kp.PubKey.Type.String() + " " + mnemonic
+}
+
+// RandomSeedToMnemonic converts a random seed to a mnemonic
+func RandomSeedToMnemonic(seed []byte) (string, error) {
+	return bip39.NewMnemonic(seed)
+}
+
+// MnemonicToRandomSeed converts a mnemonic to a random seed
+func MnemonicToRandomSeed(mnemonic string) ([]byte, error) {
+	return bip39.MnemonicToByteArray(mnemonic, true)
+}
+
+// GenerateKeypairWithRandomSeed generates a seed and generates a keypair which can be reconstructed again with the same seed
+func GenerateKeypairWithRandomSeed(kt bmcrypto.KeyType) (*bmcrypto.KeyPair, error) {
+	// Generate large enough random string
+	seed, err := bip39.NewEntropy(192)
 	if err != nil {
-		return "", "", nil, nil, err
+		return nil, err
 	}
 
-	return kt.String() + " " + mnemonic, hex.EncodeToString(e), privKey, pubKey, nil
+	return bmcrypto.CreateKeypair(kt, seed)
 }
 
 // GenerateKeypairFromMnemonic generates a keypair based on the given mnemonic
-func GenerateKeypairFromMnemonic(mnemonic string) (*bmcrypto.PrivKey, *bmcrypto.PubKey, error) {
+func GenerateKeypairFromMnemonic(mnemonic string) (*bmcrypto.KeyPair, error) {
 	words := strings.SplitN(mnemonic, " ", 2)
 
 	kt, err := bmcrypto.FindKeyType(words[0])
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
-	e, err := bip39.MnemonicToByteArray(words[1], true)
+	seed, err := MnemonicToRandomSeed(words[1])
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
-	return kt.GenerateKeyPair(bytes.NewReader(e))
+	return bmcrypto.CreateKeypair(kt, seed)
 }
diff --git a/internal/resolver/service.go b/internal/resolver/service.go
index 5dc3512..03012cf 100644
--- a/internal/resolver/service.go
+++ b/internal/resolver/service.go
@@ -22,6 +22,7 @@ package resolver
 import (
 	"crypto/sha256"
 	"encoding/base64"
+	"fmt"
 	"strconv"
 
 	"github.com/bitmaelum/bitmaelum-suite/internal/organisation"
@@ -139,12 +140,24 @@ func (s *Service) ResolveOrganisation(orgHash hash.Hash) (*OrganisationInfo, err
 
 // UploadAddressInfo uploads resolve information to one (or more) resolvers
 func (s *Service) UploadAddressInfo(info vault.AccountInfo, orgToken string) error {
+	// Read current key, so we can find the correct privkey for authentication
+	privKey := info.GetActiveKey().PrivKey
+	currentInfo, err := s.repo.ResolveAddress(info.Address.Hash())
+	if err == nil {
+		kp, err := info.FindKey(currentInfo.PublicKey.Fingerprint())
+		if err != nil {
+			return err
+		}
+		fmt.Println("found authorizing key: ", kp.FingerPrint)
+		privKey = kp.PrivKey
+	}
+
 	return s.repo.UploadAddress(*info.Address, &AddressInfo{
 		Hash:      info.Address.Hash().String(),
 		PublicKey: info.GetActiveKey().PubKey,
 		RoutingID: info.RoutingID,
 		Pow:       info.Pow.String(),
-	}, info.GetActiveKey().PrivKey, *info.Pow, orgToken)
+	}, privKey, *info.Pow, orgToken)
 }
 
 // UploadRoutingInfo uploads resolve information to one (or more) resolvers
diff --git a/internal/time.go b/internal/time.go
index 9f38057..9f09883 100644
--- a/internal/time.go
+++ b/internal/time.go
@@ -21,8 +21,10 @@ package internal
 
 import "time"
 
+var timeNow = time.Now
+
 // TimeNow returns the current time in UTC zone WITHOUT nanoseconds. This is useful when marshalling times to JSON
 func TimeNow() time.Time {
-	ct := time.Now().Unix()
+	ct := timeNow().Unix()
 	return time.Unix(ct, 0).UTC()
 }
diff --git a/internal/vault/account.go b/internal/vault/account.go
index 875a911..08cdcc9 100644
--- a/internal/vault/account.go
+++ b/internal/vault/account.go
@@ -32,11 +32,8 @@ var errAccountNotFound = errors.New("cannot find account")
 
 // KeyPair is a structure with key information
 type KeyPair struct {
-	Generator   string           `json:"generator"`   // The generator string that will generate the given keypair
-	FingerPrint string           `json:"fingerprint"` // The sha1 fingerprint for this key
-	PrivKey     bmcrypto.PrivKey `json:"priv_key"`    // PEM encoded private key
-	PubKey      bmcrypto.PubKey  `json:"pub_key"`     // PEM encoded public key
-	Active      bool             `json:"active"`      // This is the currently active key
+	bmcrypto.KeyPair
+	Active bool `json:"active"` // This is the currently active key
 }
 
 // AccountInfoV1 represents client account information
@@ -77,6 +74,25 @@ func (info AccountInfo) GetActiveKey() KeyPair {
 	return info.Keys[0]
 }
 
+// SetActiveKey sets the active key in the info
+func (info AccountInfo) SetActiveKey(kp *bmcrypto.KeyPair) {
+	for idx, k := range info.Keys {
+		info.Keys[idx].Active = k.FingerPrint == kp.FingerPrint
+	}
+}
+
+// FindKey will try and retrieve a key based on the fingerprint
+func (info AccountInfo) FindKey(fp string) (*KeyPair, error) {
+	// Find key with fingerprint
+	for _, k := range info.Keys {
+		if k.FingerPrint == fp {
+			return &k, nil
+		}
+	}
+
+	return nil, errors.New("cannot find key")
+}
+
 // AddAccount adds a new account to the vault
 func (v *Vault) AddAccount(account AccountInfo) {
 	v.Store.Accounts = append(v.Store.Accounts, account)
diff --git a/internal/vault/migrate.go b/internal/vault/migrate.go
index 3756326..00a0c26 100644
--- a/internal/vault/migrate.go
+++ b/internal/vault/migrate.go
@@ -22,6 +22,8 @@ package vault
 import (
 	"encoding/json"
 	"errors"
+
+	"github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto"
 )
 
 // MigrateVault will migrate a vault from a specific version all the way to the latest version
@@ -73,11 +75,13 @@ func MigrateVault(data []byte, fromVersion int) (*StoreType, error) {
 				accV2.Settings = accV1.Settings
 				accV2.Keys = []KeyPair{
 					{
-						Generator:   "",
-						FingerPrint: accV1.PubKey.Fingerprint(),
-						PrivKey:     accV1.PrivKey,
-						PubKey:      accV1.PubKey,
-						Active:      true,
+						KeyPair: bmcrypto.KeyPair{
+							Generator:   "",
+							FingerPrint: accV1.PubKey.Fingerprint(),
+							PrivKey:     accV1.PrivKey,
+							PubKey:      accV1.PubKey,
+						},
+						Active: true,
 					},
 				}
 				storeV2.Accounts = append(storeV2.Accounts, *accV2)
@@ -91,11 +95,13 @@ func MigrateVault(data []byte, fromVersion int) (*StoreType, error) {
 				orgV2.Validations = orgV1.Validations
 				orgV2.Keys = []KeyPair{
 					{
-						Generator:   "",
-						FingerPrint: orgV1.PubKey.Fingerprint(),
-						PrivKey:     orgV1.PrivKey,
-						PubKey:      orgV1.PubKey,
-						Active:      true,
+						KeyPair: bmcrypto.KeyPair{
+							Generator:   "",
+							FingerPrint: orgV1.PubKey.Fingerprint(),
+							PrivKey:     orgV1.PrivKey,
+							PubKey:      orgV1.PubKey,
+						},
+						Active: true,
 					},
 				}
 				storeV2.Organisations = append(storeV2.Organisations, *orgV2)
diff --git a/internal/vault/organisation.go b/internal/vault/organisation.go
index b082437..8c6044f 100644
--- a/internal/vault/organisation.go
+++ b/internal/vault/organisation.go
@@ -71,6 +71,13 @@ func (info OrganisationInfo) GetActiveKey() KeyPair {
 	return info.Keys[0]
 }
 
+// SetActiveKey sets the active key in the info
+func (info OrganisationInfo) SetActiveKey(kp *KeyPair) {
+	for _, k := range info.Keys {
+		k.Active = k.FingerPrint == kp.FingerPrint
+	}
+}
+
 // AddOrganisation adds an organisation to the vault
 func (v *Vault) AddOrganisation(organisation OrganisationInfo) {
 	v.Store.Organisations = append(v.Store.Organisations, organisation)
diff --git a/pkg/bmcrypto/bmcrypto.go b/pkg/bmcrypto/bmcrypto.go
index 0bac960..41e6250 100644
--- a/pkg/bmcrypto/bmcrypto.go
+++ b/pkg/bmcrypto/bmcrypto.go
@@ -20,7 +20,9 @@
 package bmcrypto
 
 import (
+	"bytes"
 	"crypto/rand"
+	"encoding/hex"
 	"errors"
 	"io"
 	"math/big"
@@ -39,6 +41,14 @@ type ecdsaSignature struct {
 	R, S *big.Int
 }
 
+// KeyPair is a structure with key information
+type KeyPair struct {
+	Generator   string  `json:"generator"`   // The generator string that will generate the given keypair
+	FingerPrint string  `json:"fingerprint"` // The sha1 fingerprint for this key
+	PrivKey     PrivKey `json:"priv_key"`    // PEM encoded private key
+	PubKey      PubKey  `json:"pub_key"`     // PEM encoded public key
+}
+
 // FindKeyType finds the keytype based on the given string
 func FindKeyType(typ string) (KeyType, error) {
 	for _, kt := range KeyTypes {
@@ -95,3 +105,18 @@ func Decrypt(key PrivKey, txID string, message []byte) ([]byte, error) {
 
 	return key.Type.Decrypt(key, txID, message)
 }
+
+// CreateKeypair create a new keypair
+func CreateKeypair(kt KeyType, seed []byte) (*KeyPair, error) {
+	privKey, pubKey, err := kt.GenerateKeyPair(bytes.NewReader(seed))
+	if err != nil {
+		return nil, err
+	}
+
+	return &KeyPair{
+		Generator:   hex.EncodeToString(seed),
+		FingerPrint: pubKey.Fingerprint(),
+		PrivKey:     *privKey,
+		PubKey:      *pubKey,
+	}, nil
+}
diff --git a/tools/update-resolver/main.go b/tools/update-resolver/main.go
index 8dc5671..220e0fc 100644
--- a/tools/update-resolver/main.go
+++ b/tools/update-resolver/main.go
@@ -47,8 +47,11 @@ func main() {
 
 	logrus.SetLevel(logrus.TraceLevel)
 
-	vault.VaultPassword = opts.Password
-	v := vault.OpenDefaultVault()
+	v, err := vault.Open(config.Client.Vault.Path, opts.Password)
+	if err != nil {
+		fmt.Println("cannot open vault")
+		os.Exit(1)
+	}
 
 	if opts.Address != "" {
 		addr, err := address.NewAddress(opts.Address)


Test patch:
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index d38e87a..0f32434 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -4,13 +4,14 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.13.x, 1.14.x, 1.15.x]
+        go-version: [1.13.x, 1.14.x, 1.15.x, "1.16.0-beta1" ]
         os: [ubuntu-20.04]
     runs-on: ${{ matrix.os }}
     steps:
     - name: Install Go
       uses: actions/setup-go@v2
       with:
+        stable: '!contains(${{ matrix.go }}, "beta") && !contains(${{ matrix.go }}, "rc")'
         go-version: ${{ matrix.go-version }}
     - name: Checkout code
       uses: actions/checkout@v2
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 6e54417..daeda9a 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -158,8 +158,8 @@ func TestGenerateRoutingFromMnemonic(t *testing.T) {
 	assert.NoError(t, err)
 
 	assert.Equal(t, "f5f1dc4eff7237ac0e061a9e8982b7b913fc479138189cc8d6ba5131dee1bde9", r.RoutingID)
-	assert.Equal(t, "ed25519 MC4CAQAwBQYDK2VwBCIEIDLOvf5iUAPWeNIYlbyDffgv+VA2xnS1s1mUYIOmW8XK", r.PrivateKey.String())
-	assert.Equal(t, "ed25519 MCowBQYDK2VwAyEAndS2/G3uasbaYO0+89rNzvNJ3gfOi/An1t5xvETeNoc=", r.PublicKey.String())
+	assert.Equal(t, "ed25519 MC4CAQAwBQYDK2VwBCIEIDLOvf5iUAPWeNIYlbyDffgv+VA2xnS1s1mUYIOmW8XK", r.KeyPair.PrivKey.String())
+	assert.Equal(t, "ed25519 MCowBQYDK2VwAyEAndS2/G3uasbaYO0+89rNzvNJ3gfOi/An1t5xvETeNoc=", r.KeyPair.PubKey.String())
 }
 
 func init() {
diff --git a/internal/config/routing_test.go b/internal/config/routing_test.go
index 6bc562c..8c49aba 100644
--- a/internal/config/routing_test.go
+++ b/internal/config/routing_test.go
@@ -27,16 +27,15 @@ import (
 )
 
 func TestGenerate(t *testing.T) {
-	_, r, err := GenerateRouting()
+	r, err := GenerateRouting()
 	assert.NoError(t, err)
 
 	assert.NotEmpty(t, r.RoutingID)
-	assert.NotEmpty(t, r.PrivateKey)
-	assert.NotEmpty(t, r.PublicKey)
+	assert.NotEmpty(t, r.KeyPair)
 }
 
 func TestReadSaveRouting(t *testing.T) {
-	_, r, err := GenerateRouting()
+	r, err := GenerateRouting()
 	assert.NoError(t, err)
 
 	fs = afero.NewMemMapFs()
diff --git a/internal/message/signature_test.go b/internal/message/signature_test.go
index 2a54c6a..88ff25c 100644
--- a/internal/message/signature_test.go
+++ b/internal/message/signature_test.go
@@ -280,14 +280,14 @@ func setupServer() {
 	addr, _ := address.NewAddress("foobar!")
 
 	// Note: our mail server uses key1
-	privKey, pubKey, err := testing2.ReadTestKey("../../testdata/key-1.json")
+	kp, err := testing2.ReadKeyPair("../../testdata/key-1.json")
 	if err != nil {
 		panic(err)
 	}
 	config.Routing = config.RoutingConfig{
-		RoutingID:  "12345678",
-		PrivateKey: *privKey,
-		PublicKey:  *pubKey,
+		Version:   1,
+		RoutingID: "12345678",
+		KeyPair:   kp,
 	}
 
 	// Setup container with mock repository for routing
@@ -300,7 +300,7 @@ func setupServer() {
 	uploadAddress(repo, *addr, "111100000000000000018f66a0f3591a883f2b9cc3e95a497e7cf9da1071b4cc", "12345678", "../../testdata/key-3.json")
 
 	// Note: our mail server uses key1
-	privKey, pubKey, err = testing2.ReadTestKey("../../testdata/key-1.json")
+	privKey, pubKey, err := testing2.ReadTestKey("../../testdata/key-1.json")
 	if err != nil {
 		panic(err)
 	}
diff --git a/internal/mnemonic_test.go b/internal/mnemonic_test.go
index 46af033..45ba30f 100644
--- a/internal/mnemonic_test.go
+++ b/internal/mnemonic_test.go
@@ -20,34 +20,70 @@
 package internal
 
 import (
+	"encoding/hex"
 	"testing"
 
 	"github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestGenerateED25519KeypairFromMnemonic(t *testing.T) {
-	kt, err := bmcrypto.FindKeyType("ed25519")
-	assert.NoError(t, err)
-	s, _, priv1, pub1, err := GenerateKeypairWithMnemonic(kt)
+func TestRandomSeedToMnemonic(t *testing.T) {
+	seed, _ := hex.DecodeString("cf8952d5ce19cf8d606ebf6222520a50df7bd81d33be4453")
+	mnemonic, err := RandomSeedToMnemonic(seed)
 	assert.NoError(t, err)
+	assert.Equal(t, "sort enhance rely order ostrich shop like subject giraffe barely little payment waste ugly inquiry jelly dust occur", mnemonic)
 
-	priv2, pub2, err := GenerateKeypairFromMnemonic(s)
+	tmp, err := MnemonicToRandomSeed(mnemonic)
 	assert.NoError(t, err)
+	assert.Equal(t, "cf8952d5ce19cf8d606ebf6222520a50df7bd81d33be4453", hex.EncodeToString(tmp))
+
+	tmp, err = MnemonicToRandomSeed("foo")
+	assert.Error(t, err)
+	assert.Nil(t, tmp)
+
+	tmp, err = MnemonicToRandomSeed("ed25519 foo bar baz")
+	assert.Error(t, err)
+	assert.Nil(t, tmp)
 
-	assert.Equal(t, priv1, priv2)
-	assert.Equal(t, pub1, pub2)
+	tmp, err = MnemonicToRandomSeed("ed25519 ship")
+	assert.Error(t, err)
+	assert.Nil(t, tmp)
 }
 
-func TestGenerateRSAKeypairFromMnemonic(t *testing.T) {
-	kt, err := bmcrypto.FindKeyType("rsa")
-	assert.NoError(t, err)
-	s, _, priv1, pub1, err := GenerateKeypairWithMnemonic(kt)
+func TestGetMnemonic(t *testing.T) {
+	kp := bmcrypto.KeyPair{
+		Generator:   "FOOBAR",
+		FingerPrint: "",
+		PrivKey:     bmcrypto.PrivKey{},
+		PubKey:      bmcrypto.PubKey{},
+	}
+
+	s := GetMnemonic(&kp)
+	assert.Empty(t, s)
+}
+
+func TestGenerateKeypairFromMnemonic(t *testing.T) {
+	kp, err := GenerateKeypairFromMnemonic("foobar")
+	assert.Error(t, err)
+	assert.Nil(t, kp)
+
+	kp, err = GenerateKeypairFromMnemonic("ed25519 foobar")
+	assert.Error(t, err)
+	assert.Nil(t, kp)
+
+}
+
+func TestGenerateKeypair(t *testing.T) {
+	kt, err := bmcrypto.FindKeyType("ed25519")
 	assert.NoError(t, err)
 
-	priv2, pub2, err := GenerateKeypairFromMnemonic(s)
+	kp, err := GenerateKeypairWithRandomSeed(kt)
 	assert.NoError(t, err)
 
-	assert.Equal(t, priv1, priv2)
-	assert.Equal(t, pub1, pub2)
+	mnemonic := GetMnemonic(kp)
+	assert.Contains(t, mnemonic, "ed25519")
+
+	kp2, err := GenerateKeypairFromMnemonic(mnemonic)
+	assert.NoError(t, err)
+	assert.Equal(t, kp.FingerPrint, kp2.FingerPrint)
 }
diff --git a/internal/testing/testing.go b/internal/testing/testing.go
index f8ca128..98abba8 100644
--- a/internal/testing/testing.go
+++ b/internal/testing/testing.go
@@ -26,6 +26,32 @@ import (
 	"github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto"
 )
 
+// ReadKeyPair reads a path to a keypair
+func ReadKeyPair(p string) (*bmcrypto.KeyPair, error) {
+	data, err := ioutil.ReadFile(p)
+	if err != nil {
+		return nil, err
+	}
+
+	type jsonKeyType struct {
+		PrivKey bmcrypto.PrivKey `json:"private_key"`
+		PubKey  bmcrypto.PubKey  `json:"public_key"`
+	}
+
+	v := &jsonKeyType{}
+	err = json.Unmarshal(data, &v)
+	if err != nil {
+		return nil, err
+	}
+
+	return &bmcrypto.KeyPair{
+		Generator:   "",
+		FingerPrint: v.PubKey.Fingerprint(),
+		PrivKey:     v.PrivKey,
+		PubKey:      v.PubKey,
+	}, nil
+}
+
 // ReadTestKey reads a path to a keypair and returns the keys
 func ReadTestKey(p string) (*bmcrypto.PrivKey, *bmcrypto.PubKey, error) {
 	data, err := ioutil.ReadFile(p)
diff --git a/internal/time_test.go b/internal/time_test.go
new file mode 100644
index 0000000..e71b4f1
--- /dev/null
+++ b/internal/time_test.go
@@ -0,0 +1,35 @@
+// Copyright (c) 2021 BitMaelum Authors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+package internal
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTimeNow(t *testing.T) {
+	timeNow = func() time.Time {
+		return time.Date(2010, 01, 01, 12, 34, 56, 0, time.UTC)
+	}
+
+	assert.Equal(t, "2010-01-01 12:34:56 +0000 UTC", TimeNow().String())
+}
diff --git a/internal/vault/account_test.go b/internal/vault/account_test.go
index 5f58a14..67882e3 100644
--- a/internal/vault/account_test.go
+++ b/internal/vault/account_test.go
@@ -101,11 +101,13 @@ func TestInfoToOrg(t *testing.T) {
 		FullName: "bar",
 		Keys: []KeyPair{
 			{
-				Generator:   "",
-				FingerPrint: "",
-				PrivKey:     bmcrypto.PrivKey{},
-				PubKey:      bmcrypto.PubKey{},
-				Active:      true,
+				KeyPair: bmcrypto.KeyPair{
+					Generator:   "",
+					FingerPrint: "",
+					PrivKey:     bmcrypto.PrivKey{},
+					PubKey:      bmcrypto.PubKey{},
+				},
+				Active: true,
 			},
 		},
 		Pow:         proofofwork.New(22, "foobar", 1234),
diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go
index 8538134..ea932d2 100644
--- a/internal/vault/vault_test.go
+++ b/internal/vault/vault_test.go
@@ -24,6 +24,7 @@ import (
 
 	testing2 "github.com/bitmaelum/bitmaelum-suite/internal/testing"
 	"github.com/bitmaelum/bitmaelum-suite/pkg/address"
+	"github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto"
 	"github.com/bitmaelum/bitmaelum-suite/pkg/proofofwork"
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
@@ -57,11 +58,13 @@ func TestNew(t *testing.T) {
 		Settings: nil,
 		Keys: []KeyPair{
 			{
-				Generator:   "",
-				FingerPrint: pubKey.Fingerprint(),
-				PrivKey:     *privKey,
-				PubKey:      *pubKey,
-				Active:      true,
+				KeyPair: bmcrypto.KeyPair{
+					Generator:   "",
+					FingerPrint: pubKey.Fingerprint(),
+					PrivKey:     *privKey,
+					PubKey:      *pubKey,
+				},
+				Active: true,
 			},
 		},
 		Pow:       &proofofwork.ProofOfWork{},
diff --git a/internal/wordwrap_test.go b/internal/wordwrap_test.go
index cc777c1..d1cb2e4 100644
--- a/internal/wordwrap_test.go
+++ b/internal/wordwrap_test.go
@@ -28,6 +28,7 @@ import (
 func TestWordWrap(t *testing.T) {
 	s := "NEWS PLAY ELSE CABLE UNLOCK SUSPECT TOAST MIXTURE SCARE POTTERY MONTH ESSAY IMMUNE BURGER STING RATE PLANET TOWN"
 
+	assert.Equal(t, "", WordWrap("", 40))
 	assert.Equal(t, "NEWS PLAY ELSE CABLE UNLOCK SUSPECT\nTOAST MIXTURE SCARE POTTERY MONTH ESSAY\nIMMUNE BURGER STING RATE PLANET TOWN", WordWrap(s, 40))
 	assert.Equal(t, "NEWS PLAY ELSE CABLE UNLOCK SUSPECT TOAST MIXTURE SCARE\nPOTTERY MONTH ESSAY IMMUNE BURGER STING RATE PLANET TOWN", WordWrap(s, 60))
 	assert.Equal(t, "NEWS PLAY\nELSE CABLE\nUNLOCK\nSUSPECT\nTOAST\nMIXTURE\nSCARE\nPOTTERY\nMONTH\nESSAY\nIMMUNE\nBURGER\nSTING RATE\nPLANET\nTOWN", WordWrap(s, 10))