diff --git a/actions.go b/actions.go index 63fd176eb4658d9f8daf6b48477eca54c90441fa..823e42f1afb4c6538ca6936436554c3d26c5b6a8 100644 --- a/actions.go +++ b/actions.go @@ -33,6 +33,7 @@ type Backend interface { SetResourcePassword(context.Context, string, *Resource, string) error GetUserEncryptionKeys(context.Context, *User) ([]*UserEncryptionKey, error) SetUserEncryptionKeys(context.Context, *User, []*UserEncryptionKey) error + SetUserEncryptionPublicKey(context.Context, *User, []byte) error SetApplicationSpecificPassword(context.Context, *User, *AppSpecificPasswordInfo, string) error DeleteApplicationSpecificPassword(context.Context, *User, string) error } @@ -171,12 +172,18 @@ func (s *AccountService) ChangeUserPassword(ctx context.Context, req *ChangeUser return err } - if err := req.Validate(); err != nil { + if err = req.Validate(); err != nil { return newRequestError(err) } - if err := s.updateUserEncryptionKeys(ctx, user, req.CurPassword, req.Password, UserEncryptionKeyMainID); err != nil { - return newBackendError(err) + // If the user does not yet have an encryption key, generate one now. + if !user.HasEncryptionKeys { + err = s.initializeUserEncryptionKeys(ctx, user, req.CurPassword) + } else { + err = s.updateUserEncryptionKeys(ctx, user, req.CurPassword, req.Password, UserEncryptionKeyMainID) + } + if err != nil { + return err } // Set the encrypted password attribute on the user and email resources. @@ -193,8 +200,43 @@ func (s *AccountService) ChangeUserPassword(ctx context.Context, req *ChangeUser return nil } +// Initialize the user encryption key list, by creating a new "main" key +// encrypted with the given password (which must be the primary password for the +// user). +func (s *AccountService) initializeUserEncryptionKeys(ctx context.Context, user *User, curPassword string) error { + // Generate a new key pair. + pub, priv, err := userenckey.GenerateKey() + if err != nil { + return err + } + + // Encrypt the private key with the password. + enc, err := userenckey.Encrypt(priv, []byte(curPassword)) + if err != nil { + return err + } + keys := []*UserEncryptionKey{ + &UserEncryptionKey{ + ID: UserEncryptionKeyMainID, + Key: enc, + }, + } + + // Update the backend database. + if err := s.backend.SetUserEncryptionKeys(ctx, user, keys); err != nil { + return newBackendError(err) + } + if err := s.backend.SetUserEncryptionPublicKey(ctx, user, pub); err != nil { + return newBackendError(err) + } + + return nil +} + +// Re-encrypt the specified user encryption key with newPassword. For this +// operation to succeed, we must be able to decrypt one of the keys (not +// necessarily the same one) with curPassword. func (s *AccountService) updateUserEncryptionKeys(ctx context.Context, user *User, curPassword, newPassword, keyID string) error { - // Re-encrypt the user encryption key with the new password. keys, err := s.backend.GetUserEncryptionKeys(ctx, user) if err != nil { return newBackendError(err) @@ -288,9 +330,11 @@ func (s *AccountService) CreateApplicationSpecificPassword(ctx context.Context, // Create or update the user encryption key associated with // this password. The user encryption key IDs for ASPs all // have an 'asp_' prefix, followed by the ASP ID. - keyID := "asp_" + asp.ID - if err := s.updateUserEncryptionKeys(ctx, user, req.CurPassword, password, keyID); err != nil { - return nil, err + if user.HasEncryptionKeys { + keyID := "asp_" + asp.ID + if err := s.updateUserEncryptionKeys(ctx, user, req.CurPassword, password, keyID); err != nil { + return nil, err + } } return &CreateApplicationSpecificPasswordResponse{ diff --git a/actions_test.go b/actions_test.go index dd78055f5dbcacc7aaabeae91566fab81ebd1b26..fe100bf66061880658b2cee029251681da18acd2 100644 --- a/actions_test.go +++ b/actions_test.go @@ -45,6 +45,10 @@ func (b *fakeBackend) SetUserEncryptionKeys(_ context.Context, user *User, keys return nil } +func (b *fakeBackend) SetUserEncryptionPublicKey(_ context.Context, user *User, pub []byte) error { + return nil +} + func (b *fakeBackend) SetApplicationSpecificPassword(_ context.Context, user *User, info *AppSpecificPasswordInfo, _ string) error { b.appSpecificPasswords[user.Name] = append(b.appSpecificPasswords[user.Name], info) return nil @@ -90,6 +94,10 @@ func createFakeBackend() *fakeBackend { }, }, }, + resources: make(map[string]map[string]*Resource), + passwords: make(map[string]string), + appSpecificPasswords: make(map[string][]*AppSpecificPasswordInfo), + encryptionKeys: make(map[string][]*UserEncryptionKey), } return fb } @@ -159,7 +167,7 @@ func TestService_ChangePassword(t *testing.T) { t.Fatal(err) } - if fb.passwords["testuser"] != "password" { - t.Fatalf("password was not set on the backend") + if _, ok := fb.passwords["testuser"]; !ok { + t.Fatal("password was not set on the backend") } } diff --git a/backend/model.go b/backend/model.go index 8baf77aab515886aaa7012f4b2832baf9e13f982..6befa5cdfa3348afe890cb2966d8fb58324d3e05 100644 --- a/backend/model.go +++ b/backend/model.go @@ -333,9 +333,10 @@ type ldapUserData struct { func newUser(entry *ldap.Entry) (*accountserver.User, error) { user := &accountserver.User{ - Name: entry.GetAttributeValue("uid"), - Lang: entry.GetAttributeValue("preferredLanguage"), - Has2FA: (entry.GetAttributeValue("totpSecret") != ""), + Name: entry.GetAttributeValue("uid"), + Lang: entry.GetAttributeValue("preferredLanguage"), + Has2FA: (entry.GetAttributeValue("totpSecret") != ""), + HasEncryptionKeys: (len(entry.GetAttributeValues("storageEncryptionKey")) > 0), //PasswordRecoveryHint: entry.GetAttributeValue("recoverQuestion"), } if user.Lang == "" { @@ -459,6 +460,16 @@ func (b *LDAPBackend) SetUserEncryptionKeys(ctx context.Context, user *accountse return b.conn.Modify(ctx, mod) } +func (b *LDAPBackend) SetUserEncryptionPublicKey(ctx context.Context, user *accountserver.User, pub []byte) error { + mod := ldap.NewModifyRequest(getUserDN(user)) + if user.HasEncryptionKeys { + mod.Replace("storageEncryptionPublicKey", []string{string(pub)}) + } else { + mod.Add("storageEncryptionPublicKey", []string{string(pub)}) + } + return b.conn.Modify(ctx, mod) +} + func (b *LDAPBackend) SetApplicationSpecificPassword(ctx context.Context, user *accountserver.User, info *accountserver.AppSpecificPasswordInfo, encryptedPassword string) error { emailRsrc := user.GetSingleResourceByType(accountserver.ResourceTypeEmail) if emailRsrc == nil { diff --git a/vendor/git.autistici.org/id/keystore/userenckey/gen.go b/vendor/git.autistici.org/id/keystore/userenckey/gen.go new file mode 100644 index 0000000000000000000000000000000000000000..f3cb4ceebec5f7b830829e1509f9346d32381c86 --- /dev/null +++ b/vendor/git.autistici.org/id/keystore/userenckey/gen.go @@ -0,0 +1,37 @@ +package userenckey + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" +) + +func encodePublicKeyToPEM(pub *ecdsa.PublicKey) ([]byte, error) { + der, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der}), nil +} + +// GenerateKey generates a new ECDSA key pair, and returns the +// PEM-encoded public and private key (in order). +func GenerateKey() ([]byte, []byte, error) { + pkey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + if err != nil { + return nil, nil, err + } + + privBytes, err := encodePrivateKeyToPEM(pkey) + if err != nil { + return nil, nil, err + } + pubBytes, err := encodePublicKeyToPEM(&pkey.PublicKey) + if err != nil { + return nil, nil, err + } + + return pubBytes, privBytes, nil +} diff --git a/vendor/git.autistici.org/id/keystore/userenckey/pkcs8.go b/vendor/git.autistici.org/id/keystore/userenckey/pkcs8.go new file mode 100644 index 0000000000000000000000000000000000000000..f7f61215b15bc33aa338aba28a37c699a85546b8 --- /dev/null +++ b/vendor/git.autistici.org/id/keystore/userenckey/pkcs8.go @@ -0,0 +1,17 @@ +// +build go1.10 + +package userenckey + +import ( + "crypto/ecdsa" + "encoding/pem" +) + +// Encode a private key to PEM-encoded PKCS8. +func encodePrivateKeyToPEM(priv *ecdsa.PrivateKey) ([]byte, error) { + der, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil +} diff --git a/vendor/git.autistici.org/id/keystore/userenckey/pkcs8_compat.go b/vendor/git.autistici.org/id/keystore/userenckey/pkcs8_compat.go new file mode 100644 index 0000000000000000000000000000000000000000..a63b6f8621a8910b16f0b774ef41b978f4287544 --- /dev/null +++ b/vendor/git.autistici.org/id/keystore/userenckey/pkcs8_compat.go @@ -0,0 +1,27 @@ +// +build !go1.10 + +package userenckey + +import ( + "bytes" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "os/exec" +) + +// Encode a private key to PEM-encoded PKCS8. +// +// In Go versions prior to 1.10, we must shell out to openssl to +// convert the private key to PKCS8 format. +func encodePrivateKeyToPEM(priv *ecdsa.PrivateKey) ([]byte, error) { + der, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, err + } + pkcs1 := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}) + + cmd := exec.Command("/usr/bin/openssl", "pkey") + cmd.Stdin = bytes.NewReader(pkcs1) + return cmd.Output() +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 7932f9b0121d206438bc8dfac3ef7577215637de..481def7bbc615b86c7e7aaa77700c3d38ef4cf9b 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -39,10 +39,10 @@ "revisionTime": "2018-02-18T15:46:43Z" }, { - "checksumSHA1": "hNMdjPPJ99kTEVdtM8oQvx/FoWY=", + "checksumSHA1": "rPaBG1IesiEZi/dPae7wvsWtYdc=", "path": "git.autistici.org/id/keystore/userenckey", - "revision": "128c384b1908f52c7425367d8c9579d50c639c77", - "revisionTime": "2018-02-16T21:12:09Z" + "revision": "f086b1b88dfbd16a2a4f68239d08f1195f4d9c72", + "revisionTime": "2018-04-02T09:08:18Z" }, { "checksumSHA1": "yReqUM4tQkY+1YEI+L2d0SOzFWs=",