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=",