From 037b0150810beab3cb355e1805864f1bb4775060 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 00:42:00 +0100 Subject: [PATCH 01/18] =?UTF-8?q?Account=20update=20now=20checks=20against?= =?UTF-8?q?=2010k=20=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=84=E2=96=80?= =?UTF-8?q?=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80?= =?UTF-8?q?=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=84?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=20=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=84?= =?UTF-8?q?=E2=96=80=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=84=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=20=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=84=E2=96=80=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=88=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=20=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=88=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=88=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=20=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=88=E2=96=92=E2=96=92=E2=96=84=E2=96=80=E2=96=80=E2=96=80?= =?UTF-8?q?=E2=96=80=E2=96=80=E2=96=84=E2=96=84=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=84?= =?UTF-8?q?=E2=96=84=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80?= =?UTF-8?q?=E2=96=84=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=20?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=84=E2=96=80=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=84=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=84?= =?UTF-8?q?=E2=96=92=E2=96=88=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=88=E2=96=92=E2=96=84=E2=96=88=E2=96=88?= =?UTF-8?q?=E2=96=88=E2=96=88=E2=96=88=E2=96=84=E2=96=92=E2=96=88=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=20=E2=96=91=E2=96=88=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=90=E2=96=88=E2=96=88=E2=96=84?= =?UTF-8?q?=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=8C=E2=96=92=E2=96=88?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=88=E2=96=92?= =?UTF-8?q?=E2=96=90=E2=96=88=E2=96=88=E2=96=84=E2=96=88=E2=96=88=E2=96=88?= =?UTF-8?q?=E2=96=88=E2=96=8C=E2=96=92=E2=96=88=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=20=E2=96=80=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=80=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88?= =?UTF-8?q?=E2=96=80=E2=96=92=E2=96=92=E2=96=88=E2=96=92=E2=96=91=E2=96=84?= =?UTF-8?q?=E2=96=92=E2=96=84=E2=96=88=E2=96=92=E2=96=92=E2=96=80=E2=96=88?= =?UTF-8?q?=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=80=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=88=E2=96=91=E2=96=91=20=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=90=E2=96=92=E2=96=92=E2=96=92=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=88?= =?UTF-8?q?=E2=96=92=E2=96=91=E2=96=92=E2=96=92=E2=96=80=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=88=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=88?= =?UTF-8?q?=E2=96=91=20=E2=96=92=E2=96=8C=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=84=E2=96=80=E2=96=92=E2=96=91=E2=96=92=E2=96=84?= =?UTF-8?q?=E2=96=88=E2=96=84=E2=96=88=E2=96=84=E2=96=92=E2=96=80=E2=96=84?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=8C=20=E2=96=92?= =?UTF-8?q?=E2=96=8C=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=91=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=80=E2=96=84?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=88=E2=96=8C=E2=96=8C=E2=96=8C=E2=96=8C?= =?UTF-8?q?=E2=96=8C=E2=96=88=E2=96=84=E2=96=80=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=91=E2=96=91=E2=96=91=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=90=20=E2=96=92=E2=96=90=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=8C=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=80=E2=96=88=E2=96=88=E2=96=88=E2=96=80=E2=96=92=E2=96=8C?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=8C=20?= =?UTF-8?q?=E2=96=80=E2=96=80=E2=96=84=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=8C=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=90=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=88=E2=96=91=20=E2=96=80=E2=96=84=E2=96=92?= =?UTF-8?q?=E2=96=80=E2=96=84=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=90=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=84?= =?UTF-8?q?=E2=96=84=E2=96=84=E2=96=84=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=84=E2=96=84=E2=96=80=E2=96=91=E2=96=91?= =?UTF-8?q?=20=E2=96=92=E2=96=92=E2=96=80=E2=96=84=E2=96=92=E2=96=80?= =?UTF-8?q?=E2=96=84=E2=96=80=E2=96=80=E2=96=80=E2=96=84=E2=96=80=E2=96=80?= =?UTF-8?q?=E2=96=80=E2=96=80=E2=96=84=E2=96=84=E2=96=84=E2=96=84=E2=96=84?= =?UTF-8?q?=E2=96=84=E2=96=84=E2=96=80=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=80=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=20=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=80=E2=96=84=E2=96=90=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=92?= =?UTF-8?q?=E2=96=92=E2=96=92=E2=96=92=E2=96=92=E2=96=90=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91=E2=96=91?= =?UTF-8?q?=E2=96=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _vagrant/Vagrantfile | 1 + routes/accounts.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/_vagrant/Vagrantfile b/_vagrant/Vagrantfile index 15c639f..a540a0e 100644 --- a/_vagrant/Vagrantfile +++ b/_vagrant/Vagrantfile @@ -39,6 +39,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| docker.vm.provider "virtualbox" do |v| v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] v.customize ["modifyvm", :id, "--natdnsproxy1", "on"] + v.memory = 2048 end docker.vm.provision "docker" do |d| diff --git a/routes/accounts.go b/routes/accounts.go index ff50815..6fd6d5a 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -394,6 +394,14 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { return } + if input.NewPassword != "" && !utils.IsPasswordSecure(input.NewPassword) { + utils.JSONResponse(w, 403, &AccountsUpdateResponse{ + Success: false, + Message: "Weak new password", + }) + return + } + if input.NewPassword != "" { err = user.SetPassword(input.NewPassword) if err != nil { From cab99cea73d8369ff99531fca46eccb92eeadc46 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 00:53:38 +0100 Subject: [PATCH 02/18] account create - reservations are only checked while trying to reserve --- routes/accounts.go | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/routes/accounts.go b/routes/accounts.go index 6fd6d5a..9ffd56b 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -82,6 +82,24 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { return } + if input.Username != "" { + if used, err := env.Reservations.IsUsernameUsed(input.Username); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Username already reserved", + }) + return + } + + if used, err := env.Accounts.IsUsernameUsed(input.Username); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Username already used", + }) + return + } + } + // Adding to [beta] queue if requestType[:5] == "queue" { if requestType[6:] == "reserve" { @@ -93,22 +111,6 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { }) return } - - if used, err := env.Reservations.IsUsernameUsed(input.Username); err != nil || used { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Username already reserved", - }) - return - } - - if used, err := env.Accounts.IsUsernameUsed(input.Username); err != nil || used { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Username already used", - }) - return - } } // Ensure that the email is not already used to reserve/register From dbb1ea503c925744ecb154b2511f62dd439895b8 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 00:55:08 +0100 Subject: [PATCH 03/18] account update - date modified is not updated --- routes/accounts.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/routes/accounts.go b/routes/accounts.go index 9ffd56b..e0d770b 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -2,6 +2,7 @@ package routes import ( "net/http" + "time" "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" @@ -423,6 +424,8 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { user.AltEmail = input.AltEmail } + user.DateModified = time.Now() + err = env.Accounts.UpdateID(session.Owner, user) if err != nil { env.Log.WithFields(logrus.Fields{ From 0c0d5fae23c15c20d9409701cfe91a3ccc66482e Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 00:56:00 +0100 Subject: [PATCH 04/18] token create - date modified is not updated if password scheme is outdated --- routes/tokens.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/tokens.go b/routes/tokens.go index 87d24b8..0e548d1 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -98,6 +98,7 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { // Update the user if password was updated if updated { + user.DateModified = time.Now() err := env.Accounts.UpdateID(user.ID, user) if err != nil { env.Log.WithFields(logrus.Fields{ From 3433fb9eaee4f5888edbe8d82a8e73c7b93b4d91 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 00:57:27 +0100 Subject: [PATCH 05/18] most keys methods - only body is returned --- routes/keys.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/routes/keys.go b/routes/keys.go index 4328583..68a25dc 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -18,9 +18,9 @@ import ( // KeysListResponse contains the result of the KeysList request type KeysListResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Keys *[]string `json:"keys,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Keys *[]*models.Key `json:"keys,omitempty"` } // KeysList responds with the list of keys assigned to the spiecified email @@ -54,16 +54,10 @@ func KeysList(w http.ResponseWriter, r *http.Request) { return } - // Equivalent of _.keys(keys) in JavaScript with underscore.js - keyIDs := []string{} - for _, key := range keys { - keyIDs = append(keyIDs, key.ID) - } - // Respond with list of keys utils.JSONResponse(w, 200, &KeysListResponse{ Success: true, - Keys: &keyIDs, + Keys: &keys, }) } From 560956fa2bdd2624eaec54afb60832e7f606eb7f Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 12:52:38 +0100 Subject: [PATCH 06/18] Fixed tests (hopefully) --- routes/accounts.go | 9 --------- routes/accounts_test.go | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/routes/accounts.go b/routes/accounts.go index e0d770b..6676ce3 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -208,15 +208,6 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // TODO: sanitize user name (i.e. remove caps, periods) - // Both invited and classic require an unique username, so ensure that the user with requested username isn't already used - if _, err := env.Accounts.FindAccountByName(input.Username); err == nil { - utils.JSONResponse(w, 409, &AccountsCreateResponse{ - Success: false, - Message: "Username already exists", - }) - return - } - // Both username and password are filled, so we can create a new account. account := &models.Account{ Resource: models.MakeResource("", input.Username), diff --git a/routes/accounts_test.go b/routes/accounts_test.go index 240d34a..e2ba3b6 100644 --- a/routes/accounts_test.go +++ b/routes/accounts_test.go @@ -146,12 +146,12 @@ func TestAccountsCreateInvitedExisting(t *testing.T) { // Check the result's contents require.False(t, response.Success) - require.Equal(t, "Username already exists", response.Message) + require.Equal(t, "Username already used", response.Message) } func TestAccountsCreateInvitedWeakPassword(t *testing.T) { const ( - username = "jeremy" + username = "jeremylicious" password = "c0067d4af4e87f00dbac63b6156828237059172d1bbeac67427345d6a9fda484" ) From 1c0855f533b0caf6f2d4bc6b644390ce7f64fb4f Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 13:31:17 +0100 Subject: [PATCH 07/18] account update - add yubikey support (it compiles) --- models/account.go | 21 ++++++++++++ routes/accounts.go | 82 +++++++++++++++++++++++++++++++++++++++++----- routes/tokens.go | 67 ++++++++++++++++++++++--------------- setup/setup.go | 30 ++++++++--------- 4 files changed, 150 insertions(+), 50 deletions(-) diff --git a/models/account.go b/models/account.go index 6e5f6f9..a4ec101 100644 --- a/models/account.go +++ b/models/account.go @@ -3,6 +3,7 @@ package models import ( "github.com/gyepisam/mcf" _ "github.com/gyepisam/mcf/scrypt" // Required to have mcf hash the password into scrypt + "github.com/lavab/api/factor" ) // Account stores essential data for a Lavaboom user, and is thus not encrypted. @@ -81,6 +82,26 @@ func (a *Account) VerifyPassword(password string) (bool, bool, error) { return true, false, nil } +// Verify2FA verifies the 2FA token with the account settings. +// Returns verified, challenge, error +func (a *Account) Verify2FA(factor factor.Factor, token string) (bool, string, error) { + if token == "" { + req, err := factor.Request(a.ID) + if err != nil { + return false, "", err + } + + return false, req, nil + } + + ok, err := factor.Verify(a.FactorValue, token) + if err != nil { + return false, "", err + } + + return ok, "", nil +} + // SettingsData TODO type SettingsData struct { } diff --git a/routes/accounts.go b/routes/accounts.go index 6676ce3..824c30d 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -326,16 +326,21 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { // AccountsUpdateRequest contains the input for the AccountsUpdate endpoint. type AccountsUpdateRequest struct { - AltEmail string `json:"alt_email" schema:"alt_email"` - CurrentPassword string `json:"current_password" schema:"current_password"` - NewPassword string `json:"new_password" schema:"new_password"` + AltEmail string `json:"alt_email" schema:"alt_email"` + CurrentPassword string `json:"current_password" schema:"current_password"` + NewPassword string `json:"new_password" schema:"new_password"` + FactorType string `json:"factor_type" schema:"factor_type"` + FactorValue []string `json:"factor_value" schema:"factor_value"` + Token string `json:"token" schema:"token"` } // AccountsUpdateResponse contains the result of the AccountsUpdate request. type AccountsUpdateResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Account *models.Account `json:"account"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Account *models.Account `json:"account,omitempty"` + FactorType string `json:"factor_type,omitempty"` + FactorChallenge string `json:"factor_challenge,omitempty"` } // AccountsUpdate allows changing the account's information (password etc.) @@ -348,7 +353,7 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { "error": err, }).Warn("Unable to decode a request") - utils.JSONResponse(w, 409, &AccountsUpdateResponse{ + utils.JSONResponse(w, 400, &AccountsUpdateResponse{ Success: false, Message: "Invalid input format", }) @@ -381,15 +386,57 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { } if valid, _, err := user.VerifyPassword(input.CurrentPassword); err != nil || !valid { - utils.JSONResponse(w, 409, &AccountsUpdateResponse{ + utils.JSONResponse(w, 403, &AccountsUpdateResponse{ Success: false, Message: "Invalid current password", }) return } + // Check for 2nd factor + if user.FactorType != "" { + factor, ok := env.Factors[user.FactorType] + if ok { + // Verify the 2FA + verified, challenge, err := user.Verify2FA(factor, input.Token) + if err != nil { + utils.JSONResponse(w, 500, &AccountsUpdateResponse{ + Success: false, + Message: "Internal 2FA error", + }) + + env.Log.WithFields(logrus.Fields{ + "err": err.Error(), + "factor": user.FactorType, + }).Warn("2FA authentication error") + return + } + + // Token was probably empty. Return the challenge. + if !verified && challenge != "" { + utils.JSONResponse(w, 403, &AccountsUpdateResponse{ + Success: false, + Message: "2FA token was not passed", + FactorType: user.FactorType, + FactorChallenge: challenge, + }) + return + } + + // Token was incorrect + if !verified { + utils.JSONResponse(w, 403, &AccountsUpdateResponse{ + Success: false, + Message: "Invalid token passed", + FactorType: user.FactorType, + }) + return + } + } + } + if input.NewPassword != "" && !utils.IsPasswordSecure(input.NewPassword) { - utils.JSONResponse(w, 403, &AccountsUpdateResponse{ + utils.JSONResponse(w, 400, &AccountsUpdateResponse{ Success: false, Message: "Weak new password", }) @@ -415,6 +462,23 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { user.AltEmail = input.AltEmail } + if input.FactorType != "" { + // Check if such factor exists + if _, exists := env.Factors[input.FactorType]; !exists { + utils.JSONResponse(w, 400, &AccountsUpdateResponse{ + Success: false, + Message: "Invalid new 2FA type", + }) + return + } + + user.FactorType = input.FactorType + } + + if input.FactorValue != nil && len(input.FactorValue) > 0 { + user.FactorValue = input.FactorValue + } + user.DateModified = time.Now() err = env.Accounts.UpdateID(session.Owner, user) diff --git a/routes/tokens.go b/routes/tokens.go index 0e548d1..f16c73e 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -43,11 +43,11 @@ type TokensCreateRequest struct { // TokensCreateResponse contains the result of the TokensCreate request. type TokensCreateResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Token *models.Token `json:"token,omitempty"` - FactorType string `json:"factor_type,omitempty"` - FactorRequest string `json:"factor_request,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.Token `json:"token,omitempty"` + FactorType string `json:"factor_type,omitempty"` + FactorChallenge string `json:"factor_challenge,omitempty"` } // TokensCreate allows logging in to an account. @@ -105,6 +105,8 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { "user": user.Name, "error": err, }).Error("Could not update user") + + // DO NOT RETURN! } } @@ -112,27 +114,40 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { if user.FactorType != "" { factor, ok := env.Factors[user.FactorType] if ok { - if input.Token == "" { - req, err := factor.Request(user.ID) - if err == nil { - utils.JSONResponse(w, 403, &TokensCreateResponse{ - Success: false, - Message: "Factor token was not passed", - FactorType: user.FactorType, - FactorRequest: req, - }) - return - } - } else { - ok, err := factor.Verify(user.FactorValue, input.Token) - if !ok || err != nil { - utils.JSONResponse(w, 403, &TokensCreateResponse{ - Success: false, - Message: "Invalid token passed", - FactorType: user.FactorType, - }) - return - } + // Verify the 2FA + verified, challenge, err := user.Verify2FA(factor, input.Token) + if err != nil { + utils.JSONResponse(w, 500, &TokensCreateResponse{ + Success: false, + Message: "Internal 2FA error", + }) + + env.Log.WithFields(logrus.Fields{ + "err": err.Error(), + "factor": user.FactorType, + }).Warn("2FA authentication error") + return + } + + // Token was probably empty. Return the challenge. + if !verified && challenge != "" { + utils.JSONResponse(w, 403, &TokensCreateResponse{ + Success: false, + Message: "2FA token was not passed", + FactorType: user.FactorType, + FactorChallenge: challenge, + }) + return + } + + // Token was incorrect + if !verified { + utils.JSONResponse(w, 403, &TokensCreateResponse{ + Success: false, + Message: "Invalid token passed", + FactorType: user.FactorType, + }) + return } } } diff --git a/setup/setup.go b/setup/setup.go index 06219d0..f48d726 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -90,6 +90,21 @@ func PrepareMux(flags *env.Flags) *web.Mux { // Put the RethinkDB session into the environment package env.Rethink = rethinkSession + // Initialize factors + env.Factors = make(map[string]factor.Factor) + if flags.YubiCloudID != "" { + yubicloud, err := factor.NewYubiCloud(flags.YubiCloudID, flags.YubiCloudKey) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Fatal("Unable to initiate YubiCloud") + } + env.Factors[yubicloud.Type()] = yubicloud + } + + authenticator := factor.NewAuthenticator(6) + env.Factors[authenticator.Type()] = authenticator + // Initialize the tables env.Tokens = &db.TokensTable{ RethinkCRUD: db.NewCRUDTable( @@ -235,21 +250,6 @@ func PrepareMux(flags *env.Flags) *web.Mux { env.NATS = c - // Initialize factors - env.Factors = make(map[string]factor.Factor) - if flags.YubiCloudID != "" { - yubicloud, err := factor.NewYubiCloud(flags.YubiCloudID, flags.YubiCloudKey) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Unable to initiate YubiCloud") - } - env.Factors[yubicloud.Type()] = yubicloud - } - - authenticator := factor.NewAuthenticator(6) - env.Factors[authenticator.Type()] = authenticator - // Create a new goji mux mux := web.New() From f88211345019902edde05fbaff2046107a6c5919 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 13:39:00 +0100 Subject: [PATCH 08/18] Test debug --- routes/accounts_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/accounts_test.go b/routes/accounts_test.go index e2ba3b6..de7637d 100644 --- a/routes/accounts_test.go +++ b/routes/accounts_test.go @@ -81,8 +81,8 @@ func TestAccountsCreateInvited(t *testing.T) { require.Nil(t, err) // Check the result's contents - require.True(t, response1.Success) require.Equal(t, "A new account was successfully created", response1.Message) + require.True(t, response1.Success) require.NotEmpty(t, response1.Account.ID) accountID = response1.Account.ID From cb1f614ae9344fa91ce08c5dd7ee09e1a94ef87d Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 18:09:07 +0100 Subject: [PATCH 09/18] Transformed cancer.go into contacts.go --- routes/contacts.go | 75 ++++++++++++++++++++++++----------------- routes/contacts_test.go | 32 ++++++++++++------ 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/routes/contacts.go b/routes/contacts.go index 0e46ef4..6118540 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -45,11 +45,12 @@ func ContactsList(c web.C, w http.ResponseWriter, r *http.Request) { // ContactsCreateRequest is the payload that user should pass to POST /contacts type ContactsCreateRequest struct { - Data string `json:"data" schema:"data"` - Name string `json:"name" schema:"name"` - Encoding string `json:"encoding" schema:"encoding"` - VersionMajor int `json:"version_major" schema:"version_major"` - VersionMinor int `json:"version_minor" schema:"version_minor"` + Data string `json:"data" schema:"data"` + Name string `json:"name" schema:"name"` + Encoding string `json:"encoding" schema:"encoding"` + VersionMajor int `json:"version_major" schema:"version_major"` + VersionMinor int `json:"version_minor" schema:"version_minor"` + PGPFingerprints []string `json:"pgp_fingerprints" schema:"pgp_fingerprints"` } // ContactsCreateResponse contains the result of the ContactsCreate request. @@ -80,7 +81,8 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { session := c.Env["token"].(*models.Token) // Ensure that the input data isn't empty - if input.Data == "" || input.Name == "" || input.Encoding == "" { + if input.Data == "" || input.Name == "" || input.Encoding == "" || + input.PGPFingerprints == nil || len(input.PGPFingerprints) == 0 { utils.JSONResponse(w, 400, &ContactsCreateResponse{ Success: false, Message: "Invalid request", @@ -91,11 +93,12 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Create a new contact struct contact := &models.Contact{ Encrypted: models.Encrypted{ - Encoding: input.Encoding, - Data: input.Data, - Schema: "contact", - VersionMajor: input.VersionMajor, - VersionMinor: input.VersionMinor, + Encoding: input.Encoding, + Data: input.Data, + Schema: "contact", + VersionMajor: input.VersionMajor, + VersionMinor: input.VersionMinor, + PGPFingerprints: input.PGPFingerprints, }, Resource: models.MakeResource(session.Owner, input.Name), } @@ -160,11 +163,12 @@ func ContactsGet(c web.C, w http.ResponseWriter, r *http.Request) { // ContactsUpdateRequest is the payload passed to PUT /contacts/:id type ContactsUpdateRequest struct { - Data string `json:"data" schema:"data"` - Name string `json:"name" schema:"name"` - Encoding string `json:"encoding" schema:"encoding"` - VersionMajor int `json:"version_major" schema:"version_major"` - VersionMinor int `json:"version_minor" schema:"version_minor"` + Data string `json:"data" schema:"data"` + Name string `json:"name" schema:"name"` + Encoding string `json:"encoding" schema:"encoding"` + VersionMajor *int `json:"version_major" schema:"version_major"` + VersionMinor *int `json:"version_minor" schema:"version_minor"` + PGPFingerprints []string `json:"pgp_fingerprints" schema:"pgp_fingerprints"` } // ContactsUpdateResponse contains the result of the ContactsUpdate request. @@ -213,14 +217,32 @@ func ContactsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { return } + if input.Data != "" { + contact.Data = input.Data + } + + if input.Name != "" { + contact.Name = input.Name + } + + if input.Encoding != "" { + contact.Encoding = input.Encoding + } + + if input.VersionMajor != nil { + contact.VersionMajor = *input.VersionMajor + } + + if input.VersionMinor != nil { + contact.VersionMinor = *input.VersionMinor + } + + if input.PGPFingerprints != nil { + contact.PGPFingerprints = input.PGPFingerprints + } + // Perform the update - err = env.Contacts.UpdateID(c.URLParams["id"], map[string]interface{}{ - "data": input.Data, - "name": input.Name, - "encoding": input.Encoding, - "version_major": input.VersionMajor, - "version_minor": input.VersionMinor, - }) + err = env.Contacts.UpdateID(c.URLParams["id"], input) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err, @@ -234,13 +256,6 @@ func ContactsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { return } - // Update the original struct for the response - contact.Data = input.Data - contact.Name = input.Name - contact.Encoding = input.Encoding - contact.VersionMajor = input.VersionMajor - contact.VersionMinor = input.VersionMinor - // Write the contact to the response utils.JSONResponse(w, 200, &ContactsUpdateResponse{ Success: true, diff --git a/routes/contacts_test.go b/routes/contacts_test.go index e2a5ee7..6468423 100644 --- a/routes/contacts_test.go +++ b/routes/contacts_test.go @@ -82,11 +82,12 @@ func TestContactsCreate(t *testing.T) { Uri: server.URL + "/contacts", ContentType: "application/json", Body: routes.ContactsCreateRequest{ - Data: "random stuff", - Name: "John Doe", - Encoding: "json", - VersionMajor: 1, - VersionMinor: 0, + Data: "random stuff", + Name: "John Doe", + Encoding: "json", + VersionMajor: 1, + VersionMinor: 0, + PGPFingerprints: []string{"that's totally a fingerprint!"}, }, } request.AddHeader("Authorization", "Bearer "+authToken) @@ -233,6 +234,9 @@ func TestContactsGetWrongID(t *testing.T) { } func TestContactsUpdate(t *testing.T) { + int1 := 1 + int0 := 0 + request := goreq.Request{ Method: "PUT", Uri: server.URL + "/contacts/" + contactID, @@ -241,8 +245,8 @@ func TestContactsUpdate(t *testing.T) { Data: "random stuff2", Name: "John Doez", Encoding: "json", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: &int1, + VersionMinor: &int0, }, } request.AddHeader("Authorization", "Bearer "+authToken) @@ -277,6 +281,9 @@ func TestContactsUpdateInvalid(t *testing.T) { } func TestContactsUpdateNotOwned(t *testing.T) { + int1 := 1 + int0 := 0 + request := goreq.Request{ Method: "PUT", Uri: server.URL + "/contacts/" + notOwnedContactID, @@ -285,8 +292,8 @@ func TestContactsUpdateNotOwned(t *testing.T) { Data: "random stuff2", Name: "John Doez", Encoding: "json", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: &int1, + VersionMinor: &int0, }, } request.AddHeader("Authorization", "Bearer "+authToken) @@ -302,6 +309,9 @@ func TestContactsUpdateNotOwned(t *testing.T) { } func TestContactsUpdateNotExisting(t *testing.T) { + int1 := 1 + int0 := 0 + request := goreq.Request{ Method: "PUT", Uri: server.URL + "/contacts/gibberish", @@ -310,8 +320,8 @@ func TestContactsUpdateNotExisting(t *testing.T) { Data: "random stuff2", Name: "John Doez", Encoding: "json", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: &int1, + VersionMinor: &int0, }, } request.AddHeader("Authorization", "Bearer "+authToken) From 8383ade960e2ba2192a59239ce8056f3ea9dbc14 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 18:30:14 +0100 Subject: [PATCH 10/18] Adding cache to the build --- circle.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/circle.yml b/circle.yml index e0897b4..c2e9b5e 100644 --- a/circle.yml +++ b/circle.yml @@ -3,14 +3,16 @@ machine: Europe/Berlin dependencies: + cache_directories: + - redis-2.8.18 pre: - source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list - wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - - sudo apt-get update - sudo apt-get install rethinkdb - - wget http://download.redis.io/releases/redis-2.8.18.tar.gz - - tar xvzf redis-2.8.18.tar.gz - - cd redis-2.8.18 && make + - if [[ ! -e redis-2.8.18 ]]; then wget http://download.redis.io/releases/redis-2.8.18.tar.gz && tar xvzf redis-2.8.18.tar.gz; fi + - cd redis-2.8.18 + - if [[ ! -e redis-2.8.18 ]]; then make; fi - go get github.com/apcera/gnatsd post: - rethinkdb --bind all: From b6fb38d7d1766d4c42f85e74f0b51c399e9ac4b8 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 18:32:40 +0100 Subject: [PATCH 11/18] woopsie --- circle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index c2e9b5e..29cb928 100644 --- a/circle.yml +++ b/circle.yml @@ -10,9 +10,9 @@ dependencies: - wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - - sudo apt-get update - sudo apt-get install rethinkdb - - if [[ ! -e redis-2.8.18 ]]; then wget http://download.redis.io/releases/redis-2.8.18.tar.gz && tar xvzf redis-2.8.18.tar.gz; fi + - if [[ ! -e redis-2.8.18/src/redis-server ]]; then wget http://download.redis.io/releases/redis-2.8.18.tar.gz && tar xvzf redis-2.8.18.tar.gz; fi - cd redis-2.8.18 - - if [[ ! -e redis-2.8.18 ]]; then make; fi + - if [[ ! -e redis-2.8.18/src/redis-server ]]; then make; fi - go get github.com/apcera/gnatsd post: - rethinkdb --bind all: From 283d1cf72daa9930230fa1b53dd61eba208d5d86 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 18:35:18 +0100 Subject: [PATCH 12/18] woopsie #2 --- circle.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index 29cb928..b07a904 100644 --- a/circle.yml +++ b/circle.yml @@ -11,8 +11,7 @@ dependencies: - sudo apt-get update - sudo apt-get install rethinkdb - if [[ ! -e redis-2.8.18/src/redis-server ]]; then wget http://download.redis.io/releases/redis-2.8.18.tar.gz && tar xvzf redis-2.8.18.tar.gz; fi - - cd redis-2.8.18 - - if [[ ! -e redis-2.8.18/src/redis-server ]]; then make; fi + - if [[ ! -e redis-2.8.18/src/redis-server ]]; then cd redis-2.8.18 && make; fi - go get github.com/apcera/gnatsd post: - rethinkdb --bind all: From 7e9e69df66832a1547500cd7a269306bef89308a Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 18:38:12 +0100 Subject: [PATCH 13/18] circle has weird cwd for go projects --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index b07a904..ad8a647 100644 --- a/circle.yml +++ b/circle.yml @@ -4,7 +4,7 @@ machine: dependencies: cache_directories: - - redis-2.8.18 + - /home/ubuntu/api/redis-2.8.18 pre: - source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list - wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - From ae8d1f3b614fa564afd18630754fb78ae85fb5cb Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 19:02:50 +0100 Subject: [PATCH 14/18] tokens get - cannnot get not-current-auth --- routes/tokens.go | 37 +++++++++++++++++++++++++++++-------- routes/tokens_test.go | 2 +- setup/setup.go | 1 + 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/routes/tokens.go b/routes/tokens.go index f16c73e..0bcc760 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -14,22 +14,43 @@ import ( // TokensGetResponse contains the result of the TokensGet request. type TokensGetResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Created *time.Time `json:"created,omitempty"` - Expires *time.Time `json:"expires,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.Token `json:"token,omitempty"` } // TokensGet returns information about the current token. func TokensGet(c web.C, w http.ResponseWriter, r *http.Request) { - // Fetch the current session from the database - session := c.Env["token"].(*models.Token) + // Initialize + var ( + token *models.Token + err error + ) + + id, ok := c.URLParams["id"] + if !ok || id == "" { + // Get the token from the middleware + token = c.Env["token"].(*models.Token) + } else { + token, err = env.Tokens.GetToken(id) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "id": id, + }).Warn("Unable to find the token") + + utils.JSONResponse(w, 404, &TokensGetResponse{ + Success: false, + Message: "Invalid token ID", + }) + return + } + } // Respond with the token information utils.JSONResponse(w, 200, &TokensGetResponse{ Success: true, - Created: &session.DateCreated, - Expires: &session.ExpiryDate, + Token: token, }) } diff --git a/routes/tokens_test.go b/routes/tokens_test.go index 6fcf270..487de7e 100644 --- a/routes/tokens_test.go +++ b/routes/tokens_test.go @@ -181,7 +181,7 @@ func TestTokensGet(t *testing.T) { require.Nil(t, err) require.True(t, response.Success) - require.True(t, response.Expires.After(time.Now().UTC())) + require.True(t, response.Token.ExpiryDate.After(time.Now().UTC())) } func TestTokensDeleteById(t *testing.T) { diff --git a/setup/setup.go b/setup/setup.go index f48d726..ba66e99 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -283,6 +283,7 @@ func PrepareMux(flags *env.Flags) *web.Mux { // Tokens auth.Get("/tokens", routes.TokensGet) + auth.Get("/tokens/:id", routes.TokensGet) mux.Post("/tokens", routes.TokensCreate) auth.Delete("/tokens", routes.TokensDelete) auth.Delete("/tokens/:id", routes.TokensDelete) From d8653a3ed6ce06d67d69307d040cab9fd3a028cd Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 19:15:03 +0100 Subject: [PATCH 15/18] Get by email --- models/key.go | 1 - routes/keys.go | 97 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/models/key.go b/models/key.go index b114ab8..c8adc1c 100644 --- a/models/key.go +++ b/models/key.go @@ -7,7 +7,6 @@ type Key struct { //Body []byte `json:"body" gorethink:"body"` // Raw key contents Headers map[string]string `json:"headers" gorethink:"headers"` // Headers passed with the key - Email string `json:"email" gorethink:"email"` // Address associated with the key Algorithm string `json:"algorithm" gorethink:"algorithm"` // Algorithm of the key Length uint16 `json:"length" gorethink:"length"` // Length of the key Key string `json:"key" gorethink:"key"` // Armor-encoded key diff --git a/routes/keys.go b/routes/keys.go index 68a25dc..b86ff27 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -200,21 +200,94 @@ type KeysGetResponse struct { // KeysGet does *something* - TODO func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { + // Initialize vars + var ( + key *models.Key + ) + // Get ID from the passed URL params id := c.URLParams["id"] - // Fetch the requested key from the database - key, err := env.Keys.FindByFingerprint(id) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - }).Warn("Unable to fetch the requested key from the database") - - utils.JSONResponse(w, 404, &KeysGetResponse{ - Success: false, - Message: "Requested key does not exist on our server", - }) - return + // Check if ID is an email or a fingerprint. + // Fingerprints can't contain @, right? + if strings.Contains(id, "@") { + // Who cares about the second part? I don't! + username := strings.Split(id, "@")[0] + + // Resolve account + account, err := env.Accounts.FindAccountByName(username) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "name": username, + }).Warn("Unable to fetch the requested account from the database") + + utils.JSONResponse(w, 404, &KeysGetResponse{ + Success: false, + Message: "No such user", + }) + return + } + + // Does the user have a default PGP key set? + if account.PGPKey != "" { + // Fetch the requested key from the database + key2, err := env.Keys.FindByFingerprint(account.PGPKey) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to fetch the requested key from the database") + + utils.JSONResponse(w, 500, &KeysGetResponse{ + Success: false, + Message: "Invalid user public key ID", + }) + return + } + + key = key2 + } else { + keys, err := env.Keys.FindByOwner(account.ID) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "owner": account.ID, + }).Warn("Unable to fetch user's keys from the database") + + utils.JSONResponse(w, 500, &KeysGetResponse{ + Success: false, + Message: "Cannot find keys assigned to the account", + }) + return + } + + if len(keys) == 0 { + utils.JSONResponse(w, 500, &KeysGetResponse{ + Success: false, + Message: "Account has no keys assigned to itself", + }) + return + } + + // i should probably sort them? + key = keys[0] + } + } else { + // Fetch the requested key from the database + key2, err := env.Keys.FindByFingerprint(id) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to fetch the requested key from the database") + + utils.JSONResponse(w, 404, &KeysGetResponse{ + Success: false, + Message: "Requested key does not exist on our server", + }) + return + } + + key = key2 } // Return the requested key From 8912053df13e6903cf7e8743c9aa2f64c3f63987 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 19:23:06 +0100 Subject: [PATCH 16/18] emails creation changes --- routes/emails.go | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/routes/emails.go b/routes/emails.go index 612324b..1752f0d 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -121,15 +121,21 @@ func EmailsList(c web.C, w http.ResponseWriter, r *http.Request) { } type EmailsCreateRequest struct { - To []string `json:"to"` - BCC []string `json:"bcc"` - ReplyTo string `json:"reply_to"` - ThreadID string `json:"thread_id"` - Subject string `json:"title"` - Body string `json:"body"` - Preview string `json:"preview"` - Attachments []string `json:"attachments"` - PGPFingerprints []string `json:"pgp_fingerprints"` + To []string `json:"to"` + BCC []string `json:"bcc"` + ReplyTo string `json:"reply_to"` + ThreadID string `json:"thread_id"` + Subject string `json:"title"` + IsEncrypted bool `json:"is_encrypted"` + Body string `json:"body"` + BodyVersionMajor int `json:"body_version_major"` + BodyVersionMinor int `json:"body_version_minor"` + Preview string `json:"preview"` + PreviewVersionMajor int `json:"preview_version_major"` + PreviewVersionMinor int `json:"preview_version_minor"` + Encoding string `json:"encoding"` + Attachments []string `json:"attachments"` + PGPFingerprints []string `json:"pgp_fingerprints"` } // EmailsCreateResponse contains the result of the EmailsCreate request. @@ -179,19 +185,19 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { PGPFingerprints: input.PGPFingerprints, Data: input.Body, Schema: "email_body", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: input.BodyVersionMajor, + VersionMinor: input.BodyVersionMinor, }, Preview: models.Encrypted{ Encoding: "json", PGPFingerprints: input.PGPFingerprints, Data: input.Preview, Schema: "email_preview", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: input.PreviewVersionMajor, + VersionMinor: input.PreviewVersionMinor, }, ThreadID: input.ThreadID, - Status: "queued", + Status: "queued;" + strconv.FormatBool(input.IsEncrypted), } // Insert the email into the database From dd3a57c6368f28c659da504ce8f0e4f16c4a1406 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Fri, 2 Jan 2015 23:57:11 +0100 Subject: [PATCH 17/18] Changed POST /emails according to Andrei --- routes/emails.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/routes/emails.go b/routes/emails.go index 1752f0d..8f0222b 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -126,7 +126,6 @@ type EmailsCreateRequest struct { ReplyTo string `json:"reply_to"` ThreadID string `json:"thread_id"` Subject string `json:"title"` - IsEncrypted bool `json:"is_encrypted"` Body string `json:"body"` BodyVersionMajor int `json:"body_version_major"` BodyVersionMinor int `json:"body_version_minor"` @@ -197,7 +196,7 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { VersionMinor: input.PreviewVersionMinor, }, ThreadID: input.ThreadID, - Status: "queued;" + strconv.FormatBool(input.IsEncrypted), + Status: "queued", } // Insert the email into the database From adad7a5bf4fccc54f7e518b175e29edb22c20a31 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 3 Jan 2015 00:45:06 +0100 Subject: [PATCH 18/18] Avatars support (testless) --- routes/avatars.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++ setup/setup.go | 3 ++ 2 files changed, 89 insertions(+) create mode 100644 routes/avatars.go diff --git a/routes/avatars.go b/routes/avatars.go new file mode 100644 index 0000000..c050f6b --- /dev/null +++ b/routes/avatars.go @@ -0,0 +1,86 @@ +package routes + +import ( + "crypto/md5" + "encoding/hex" + "image/color" + "image/png" + "net/http" + "strconv" + + "github.com/lavab/api/utils" + + "github.com/cupcake/sigil/gen" + "github.com/zenazn/goji/web" +) + +var avatarConfig = gen.Sigil{ + Rows: 5, + Foreground: []color.NRGBA{ + color.NRGBA{45, 79, 255, 255}, + color.NRGBA{254, 180, 44, 255}, + color.NRGBA{226, 121, 234, 255}, + color.NRGBA{30, 179, 253, 255}, + color.NRGBA{232, 77, 65, 255}, + color.NRGBA{49, 203, 115, 255}, + color.NRGBA{141, 69, 170, 255}, + }, + Background: color.NRGBA{224, 224, 224, 255}, +} + +func Avatars(c web.C, w http.ResponseWriter, r *http.Request) { + // Parse the query params + query := r.URL.Query() + + // Settings + var ( + widthString = query.Get("width") + width int + ) + + // Read width + if widthString == "" { + width = 100 + } else { + var err error + width, err = strconv.Atoi(widthString) + if err != nil { + utils.JSONResponse(w, 400, map[string]interface{}{ + "message": "Invalid width", + }) + return + } + } + + hash := c.URLParams["hash"] + ext := c.URLParams["ext"] + + if ext != "png" && ext != "svg" { + ext = "png" + } + + // data to parse + var data []byte + + // md5 hash + if len(hash) == 32 { + data, _ = hex.DecodeString(hash) + } + + // not md5 + if data == nil { + hashed := md5.Sum([]byte(hash)) + data = hashed[:] + } + + // if svg + if ext == "svg" { + w.Header().Set("Content-Type", "image/svg+xml") + avatarConfig.MakeSVG(w, width, false, data) + return + } + + // generate the png + w.Header().Set("Content-Type", "image/png") + png.Encode(w, avatarConfig.Make(width, false, data)) +} diff --git a/setup/setup.go b/setup/setup.go index ba66e99..44ec365 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -281,6 +281,9 @@ func PrepareMux(flags *env.Flags) *web.Mux { auth.Delete("/accounts/:id", routes.AccountsDelete) auth.Post("/accounts/:id/wipe-data", routes.AccountsWipeData) + // Avatars + mux.Get("/avatars/:hash.:ext", routes.Avatars) + // Tokens auth.Get("/tokens", routes.TokensGet) auth.Get("/tokens/:id", routes.TokensGet)