Repository: bitmaelum/bitmaelum-suite
Pull request #37 (2020-10-07 18:31:37)

Using mnemonic phrases to generate mailserver routing id and key
Using mnemonic phrases to generate mailserver routing id and key


Patch:
diff --git a/cmd/bm-client/handlers/create_organisation.go b/cmd/bm-client/handlers/create_organisation.go
index 62d884a..0c39ff5 100644
--- a/cmd/bm-client/handlers/create_organisation.go
+++ b/cmd/bm-client/handlers/create_organisation.go
@@ -38,6 +38,8 @@ func CreateOrganisation(vault *vault.Vault, orgName, fullName string, orgValidat
 	}
 	fmt.Printf("not found. This is a good thing.\n")
 
+	var seed string
+
 	fmt.Printf("* Checking if the organisation is already present in the vault: ")
 	var info *internal.OrganisationInfo
 	if vault.HasOrganisation(orgAddr) {
@@ -48,7 +50,12 @@ func CreateOrganisation(vault *vault.Vault, orgName, fullName string, orgValidat
 		fmt.Printf("not found. This is a good thing.\n")
 
 		fmt.Printf("* Generating organisation public/private key pair: ")
-		privKey, pubKey, err := bmcrypto.GenerateKeyPair(bmcrypto.KeyTypeRSA)
+
+		var (
+			privKey *bmcrypto.PrivKey
+			pubKey  *bmcrypto.PubKey
+		)
+		seed, privKey, pubKey, err = bmcrypto.GenerateKeypairWithSeed()
 		if err != nil {
 			fmt.Print(err)
 			fmt.Println("")
@@ -95,4 +102,25 @@ func CreateOrganisation(vault *vault.Vault, orgName, fullName string, orgValidat
 
 	fmt.Printf("\n")
 	fmt.Printf("* All done")
+
+	if len(seed) > 0 {
+		fmt.Print(`
+*****************************************************************************
+!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORT
+*****************************************************************************
+
+We have generated a private key which allows you to control the organisation. 
+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(seed, 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-config/cmd/generate_routing_id.go b/cmd/bm-config/cmd/generate_routing_id.go
index f22b3d9..b1239a8 100644
--- a/cmd/bm-config/cmd/generate_routing_id.go
+++ b/cmd/bm-config/cmd/generate_routing_id.go
@@ -1,8 +1,10 @@
 package cmd
 
 import (
+	"fmt"
 	"os"
 
+	"github.com/bitmaelum/bitmaelum-suite/internal"
 	"github.com/bitmaelum/bitmaelum-suite/internal/config"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -28,7 +30,7 @@ This command creates a new routing file if one does not exist.`,
 		}
 
 		// Generate new routing
-		r, err := config.Generate()
+		seed, r, err := config.GenerateRouting()
 		if err != nil {
 			logrus.Fatalf("Error while generating routing file: %v", err)
 		}
@@ -40,6 +42,23 @@ This command creates a new routing file if one does not exist.`,
 		}
 
 		logrus.Printf("Generated routing file: %s", config.Server.Server.RoutingFile)
+
+		fmt.Print(`
+*****************************************************************************
+!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORTANT!IMPORT
+*****************************************************************************
+
+We have generated a private key which allows you to control your server. 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(seed, 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.
+`)
 	},
 }
 
diff --git a/go.mod b/go.mod
index 15a2a4a..22f340c 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,6 @@ require (
 	github.com/gorilla/mux v1.7.4
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/jessevdk/go-flags v1.4.0
-	github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b // indirect
 	github.com/mattn/go-colorable v0.1.7 // indirect
 	github.com/mattn/go-runewidth v0.0.8 // indirect
 	github.com/mattn/go-sqlite3 v1.14.1
@@ -28,13 +27,14 @@ require (
 	github.com/spf13/afero v1.1.2
 	github.com/spf13/cobra v1.0.0
 	github.com/stretchr/testify v1.6.1
+	github.com/tyler-smith/go-bip39 v1.0.2
 	github.com/vtolstov/jwt-go v3.2.0+incompatible
 	github.com/x-cray/logrus-prefixed-formatter v0.5.2
 	github.com/xhit/go-str2duration/v2 v2.0.0
 	github.com/zalando/go-keyring v0.1.0
 	golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
+	golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
 	golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
-	golang.org/x/tools v0.0.0-20201002055958-0d28ed0cbe40 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	gopkg.in/yaml.v2 v2.3.0
-	honnef.co/go/tools v0.0.1-2020.1.5 // indirect
 )
diff --git a/go.sum b/go.sum
index 99c388d..bcc7114 100644
--- a/go.sum
+++ b/go.sum
@@ -88,7 +88,6 @@ github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gookit/color v1.2.7 h1:4qePMNWZhrmbfYJDix+J4V2l0iVW+6jQGjicELlN14E=
@@ -112,8 +111,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b h1:FQ7+9fxhyp82ks9vAuyPzG0/vVbWwMwLJ+P6yJI5FN8=
-github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b/go.mod h1:HMcgvsgd0Fjj4XXDkbjdmlbI505rUPBs6WBMYg2pXks=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -173,7 +170,6 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -202,6 +198,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tyler-smith/go-bip39 v1.0.2 h1:+t3w+KwLXO6154GNJY+qUtIxLTmFjfUmpguQT1OlOT8=
+github.com/tyler-smith/go-bip39 v1.0.2/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/vtolstov/jwt-go v3.2.0+incompatible h1:N99J+L6BWzi4Gqh4S8abUeFS+NxXUR+5awx0XttOflM=
 github.com/vtolstov/jwt-go v3.2.0+incompatible/go.mod h1:bL8KYtkAu43YFxd/24hbmyrb+j4EiO1Ic23vLUIFwmg=
@@ -211,7 +209,6 @@ github.com/xhit/go-str2duration/v2 v2.0.0 h1:uFtk6FWB375bP7ewQl+/1wBcn840GPhnySO
 github.com/xhit/go-str2duration/v2 v2.0.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/zalando/go-keyring v0.1.0 h1:ffq972Aoa4iHNzBlUHgK5Y+k8+r/8GvcGd80/OFZb/k=
 github.com/zalando/go-keyring v0.1.0/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -222,7 +219,6 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -238,11 +234,8 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -296,15 +289,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
 golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c h1:9BSeO6440XJVa2mxIcRAndAol4g4g2KflCVGcHx9Yu8=
-golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
-golang.org/x/tools v0.0.0-20201002055958-0d28ed0cbe40 h1:ErPN1Z9An7dXc56pRUCKgWJkjYzc3hE+y15ky9E8qxU=
-golang.org/x/tools v0.0.0-20201002055958-0d28ed0cbe40/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -334,7 +320,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
@@ -351,5 +336,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75k=
-honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
diff --git a/internal/config/routing.go b/internal/config/routing.go
index b8b89cf..a06ca26 100644
--- a/internal/config/routing.go
+++ b/internal/config/routing.go
@@ -1,13 +1,14 @@
 package config
 
 import (
+	"crypto/ed25519"
+	"encoding/hex"
 	"encoding/json"
 	"io/ioutil"
 	"path/filepath"
 
 	"github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto"
 	"github.com/bitmaelum/bitmaelum-suite/pkg/hash"
-	"github.com/google/uuid"
 	"github.com/spf13/afero"
 )
 
@@ -55,19 +56,15 @@ func SaveRouting(p string, routing *Routing) error {
 }
 
 // Generate generates a new routing structure
-func Generate() (*Routing, error) {
-	id, err := uuid.NewRandom()
+func GenerateRouting() (string, *Routing, error) {
+	seed, privKey, pubKey, err := bmcrypto.GenerateKeypairWithSeed()
 	if err != nil {
-		return nil, err
+		return "", nil, err
 	}
 
-	privKey, pubKey, err := bmcrypto.GenerateKeyPair(bmcrypto.KeyTypeRSA)
-	if err != nil {
-		return nil, err
-	}
-
-	return &Routing{
-		RoutingID:  hash.New(id.String()).String(),
+	id := hex.EncodeToString(pubKey.K.(ed25519.PublicKey))
+	return seed, &Routing{
+		RoutingID:  hash.New(id).String(),
 		PrivateKey: *privKey,
 		PublicKey:  *pubKey,
 	}, nil
diff --git a/internal/wordwrap.go b/internal/wordwrap.go
new file mode 100644
index 0000000..2a5fcbd
--- /dev/null
+++ b/internal/wordwrap.go
@@ -0,0 +1,27 @@
+package internal
+
+import "strings"
+
+func WordWrap(s string, limit int) string {
+	if strings.TrimSpace(s) == "" {
+		return s
+	}
+
+	words := strings.Fields(strings.ToUpper(s))
+
+	var result, line string
+	for len(words) > 0 {
+		if len(line)+len(words[0]) > limit {
+			result += strings.TrimSpace(line) + "\n"
+			line = ""
+		}
+
+		line = line + words[0] + " "
+		words = words[1:]
+	}
+	if line != "" {
+		result += strings.TrimSpace(line)
+	}
+
+	return result
+}
diff --git a/pkg/bmcrypto/dual_keyexchange.go b/pkg/bmcrypto/dual_keyexchange.go
index 410cdcf..e3c085c 100644
--- a/pkg/bmcrypto/dual_keyexchange.go
+++ b/pkg/bmcrypto/dual_keyexchange.go
@@ -19,7 +19,7 @@ type TransactionId struct {
 	R []byte
 }
 
-func (txId TransactionId)ToHex() string {
+func (txId TransactionId) ToHex() string {
 	return hex.EncodeToString(bytes.Join([][]byte{txId.P, txId.R}, []byte{}))
 }
 
@@ -59,8 +59,8 @@ func DualKeyExchange(pub PubKey) ([]byte, *TransactionId, error) {
 		return nil, nil, err
 	}
 
-	f := hs(D)          // Step 2: f = Hs(D)
-	P := f.Public()	    // Step 3-5: convert F into private Key (F=fG)
+	f := hs(D)      // Step 2: f = Hs(D)
+	P := f.Public() // Step 3-5: convert F into private Key (F=fG)
 
 	return D, &TransactionId{
 		P: P.(ed25519.PublicKey)[:32],
@@ -84,8 +84,8 @@ func DualKeyGetSecret(priv PrivKey, txId TransactionId) ([]byte, bool, error) {
 		return nil, false, err
 	}
 
-	fprime := hs(Dprime)        // Step 4: f' = Hs(D')
-	Pprime := fprime.Public()   // Step 5-6: P = F' = f'G
+	fprime := hs(Dprime)      // Step 4: f' = Hs(D')
+	Pprime := fprime.Public() // Step 5-6: P = F' = f'G
 
 	// If Pprime = P, then we know both alice and bob uses D (and Dprime) as the secret
 	if subtle.ConstantTimeCompare(txId.P, Pprime.(ed25519.PublicKey)) == 1 {
@@ -96,7 +96,7 @@ func DualKeyGetSecret(priv PrivKey, txId TransactionId) ([]byte, bool, error) {
 }
 
 func generateRandomScalar() []byte {
-    // get 256bits/32bytes of random data
+	// get 256bits/32bytes of random data
 	buf := make([]byte, 32)
 	_, err := io.ReadFull(randReader, buf)
 	if err != nil {
@@ -312,4 +312,3 @@ func scReduce32(s *[32]byte) {
 	s[30] = byte(s11 >> 9)
 	s[31] = byte(s11 >> 17)
 }
-
diff --git a/pkg/bmcrypto/seed.go b/pkg/bmcrypto/seed.go
new file mode 100644
index 0000000..e17b9b5
--- /dev/null
+++ b/pkg/bmcrypto/seed.go
@@ -0,0 +1,65 @@
+package bmcrypto
+
+import (
+	"crypto/ed25519"
+	"crypto/sha256"
+	"io"
+
+	"github.com/tyler-smith/go-bip39"
+	"golang.org/x/crypto/hkdf"
+)
+
+// GenerateKeypairFromSeed generates a keypair based on the given seed
+func GenerateKeypairFromSeed(seed string) (*PrivKey, *PubKey, error) {
+	e, err := bip39.MnemonicToByteArray(seed, true)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return genKey(e)
+}
+
+// GenerateKeypairWithSeed generates a seed, and a keypair that can be generated through the same seed again.
+func GenerateKeypairWithSeed() (string, *PrivKey, *PubKey, error) {
+	// Generate large enough random string
+	e, err := bip39.NewEntropy(192)
+	if err != nil {
+		return "", nil, nil, err
+	}
+
+	// Generate seed words
+	seed, err := bip39.NewMnemonic(e)
+	if err != nil {
+		return "", nil, nil, err
+	}
+
+	privKey, pubKey, err := genKey(e)
+	if err != nil {
+		return "", nil, nil, err
+	}
+
+	return seed, privKey, pubKey, nil
+}
+
+func genKey(e []byte) (*PrivKey, *PubKey, error) {
+	// Stretch 192 bits to 256 bits
+	rd := hkdf.New(sha256.New, e, []byte{}, []byte{})
+	expbuf := make([]byte, 32)
+	_, err := io.ReadFull(rd, expbuf)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Generate keypair
+	r := ed25519.NewKeyFromSeed(expbuf[:32])
+	privKey, err := NewPrivKeyFromInterface(r)
+	if err != nil {
+		return nil, nil, err
+	}
+	pubKey, err := NewPubKeyFromInterface(r.Public())
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return privKey, pubKey, nil
+}


Test patch:
diff --git a/internal/config/routing_test.go b/internal/config/routing_test.go
index aa058bd..9b67067 100644
--- a/internal/config/routing_test.go
+++ b/internal/config/routing_test.go
@@ -8,7 +8,7 @@ import (
 )
 
 func TestGenerate(t *testing.T) {
-	r, err := Generate()
+	_, r, err := GenerateRouting()
 	assert.NoError(t, err)
 
 	assert.NotEmpty(t, r.RoutingID)
@@ -17,7 +17,7 @@ func TestGenerate(t *testing.T) {
 }
 
 func TestReadSaveRouting(t *testing.T) {
-	r, err := Generate()
+	_, r, err := GenerateRouting()
 	assert.NoError(t, err)
 
 	fs = afero.NewMemMapFs()
diff --git a/internal/wordwrap_test.go b/internal/wordwrap_test.go
new file mode 100644
index 0000000..0b13d33
--- /dev/null
+++ b/internal/wordwrap_test.go
@@ -0,0 +1,16 @@
+package internal
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_WordWrap(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, "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))
+	assert.Equal(t, "\nNEWS\nPLAY\nELSE\nCABLE\nUNLOCK\nSUSPECT\nTOAST\nMIXTURE\nSCARE\nPOTTERY\nMONTH\nESSAY\nIMMUNE\nBURGER\nSTING\nRATE\nPLANET\nTOWN", WordWrap(s, 3))
+}
diff --git a/pkg/bmcrypto/dual_keyexchange_test.go b/pkg/bmcrypto/dual_keyexchange_test.go
index 0bb28ce..edab925 100644
--- a/pkg/bmcrypto/dual_keyexchange_test.go
+++ b/pkg/bmcrypto/dual_keyexchange_test.go
@@ -18,7 +18,6 @@ func TestTransactionId(t *testing.T) {
 	assert.Equal(t, []byte{120, 216, 242, 172, 92, 92, 168, 227, 245, 83, 247, 191, 240, 109, 136, 59, 230, 226, 60, 74, 192, 193, 188, 164, 195, 112, 46, 42, 177, 238, 134, 210}, txid.P)
 	assert.Equal(t, []byte{4, 173, 159, 128, 130, 144, 107, 116, 74, 189, 217, 50, 76, 127, 250, 119, 30, 51, 208, 135, 247, 127, 92, 44, 255, 60, 131, 211, 92, 187, 57, 65}, txid.R)
 
-
 	txid, err = TxIdFromString("78d8f2ac5c5ca8e3f553f7bff06d883be6e23c4ac0c1bca4c3702e2ab1ee86d204ad9f8082906b744a")
 	assert.Error(t, err)
 	assert.Nil(t, txid)
@@ -51,7 +50,6 @@ func TestDualKeyExchange(t *testing.T) {
 	assert.True(t, ok)
 	assert.Equal(t, D, Dprime)
 
-
 	privKey2, _ := NewPrivKey("ed25519 MC4CAQAwBQYDK2VwBCIEII6nA1nsVQu1Pid+CoH6yxw9Z2yOU9++S30awQvIW3m/")
 	Dprime, ok, err = DualKeyGetSecret(*privKey2, *txId)
 	assert.NoError(t, err)
diff --git a/pkg/bmcrypto/seed_test.go b/pkg/bmcrypto/seed_test.go
new file mode 100644
index 0000000..9169ace
--- /dev/null
+++ b/pkg/bmcrypto/seed_test.go
@@ -0,0 +1,18 @@
+package bmcrypto
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGenerateKeypairFromSeed(t *testing.T) {
+	s, priv1, pub1, err := GenerateKeypairWithSeed()
+	assert.NoError(t, err)
+
+	priv2, pub2, err := GenerateKeypairFromSeed(s)
+	assert.NoError(t, err)
+
+	assert.Equal(t, priv1, priv2)
+	assert.Equal(t, pub1, pub2)
+}