Commit 21fb9b21 authored by ale's avatar ale

Use the unified composite type package from ai3/go-common

Avoid implementing our own version of asp/userenckey/u2f serialization
and deserialization.
parent a5907c63
Pipeline #2556 passed with stages
in 5 minutes and 39 seconds
......@@ -8,6 +8,7 @@ import (
"strings"
"testing"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
"git.autistici.org/ai3/go-common/pwhash"
sso "git.autistici.org/id/go-sso"
)
......@@ -20,7 +21,7 @@ type fakeBackend struct {
passwords map[string]string
recoveryPasswords map[string]string
appSpecificPasswords map[string][]*AppSpecificPasswordInfo
encryptionKeys map[string][]*UserEncryptionKey
encryptionKeys map[string][]*ct.EncryptedKey
}
func (b *fakeBackend) NewTransaction() (TX, error) {
......@@ -148,11 +149,11 @@ func (b *fakeBackend) SetResourcePassword(_ context.Context, r *Resource, passwo
return nil
}
func (b *fakeBackend) GetUserEncryptionKeys(_ context.Context, user *User) ([]*UserEncryptionKey, error) {
func (b *fakeBackend) GetUserEncryptionKeys(_ context.Context, user *User) ([]*ct.EncryptedKey, error) {
return b.encryptionKeys[user.Name], nil
}
func (b *fakeBackend) SetUserEncryptionKeys(_ context.Context, user *User, keys []*UserEncryptionKey) error {
func (b *fakeBackend) SetUserEncryptionKeys(_ context.Context, user *User, keys []*ct.EncryptedKey) error {
b.encryptionKeys[user.Name] = keys
b.users[user.Name].HasEncryptionKeys = true
return nil
......@@ -233,7 +234,7 @@ func createFakeBackend() *fakeBackend {
passwords: make(map[string]string),
recoveryPasswords: make(map[string]string),
appSpecificPasswords: make(map[string][]*AppSpecificPasswordInfo),
encryptionKeys: make(map[string][]*UserEncryptionKey),
encryptionKeys: make(map[string][]*ct.EncryptedKey),
}
fb.addUser(&User{
Name: testUser,
......
package backend
import (
"errors"
"fmt"
"strings"
as "git.autistici.org/ai3/accountserver"
"github.com/tstranex/u2f"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
)
// Extend the AppSpecificPasswordInfo type, which only contains public
// information, with the encrypted password.
type appSpecificPassword struct {
as.AppSpecificPasswordInfo
Password string
}
func (p *appSpecificPassword) Encode() string {
return strings.Join([]string{
p.Service,
p.Password,
p.Comment,
}, ":")
}
func newAppSpecificPassword(info as.AppSpecificPasswordInfo, pw string) *appSpecificPassword {
return &appSpecificPassword{
AppSpecificPasswordInfo: info,
Password: pw,
func newAppSpecificPassword(info as.AppSpecificPasswordInfo, pw string) *ct.AppSpecificPassword {
return &ct.AppSpecificPassword{
ID: info.ID,
Service: info.Service,
Comment: info.Comment,
EncryptedPassword: pw,
}
}
func parseAppSpecificPassword(asp string) (*appSpecificPassword, error) {
parts := strings.Split(asp, ":")
if len(parts) != 3 {
return nil, errors.New("badly encoded app-specific password")
func decodeAppSpecificPasswords(values []string) []*ct.AppSpecificPassword {
var out []*ct.AppSpecificPassword
for _, value := range values {
if asp, err := ct.UnmarshalAppSpecificPassword(value); err == nil {
out = append(out, asp)
}
}
return newAppSpecificPassword(as.AppSpecificPasswordInfo{
Service: parts[0],
Comment: parts[2],
}, parts[1]), nil
return out
}
func decodeAppSpecificPasswords(values []string) []*appSpecificPassword {
var out []*appSpecificPassword
for _, value := range values {
if asp, err := parseAppSpecificPassword(value); err == nil {
func excludeASPFromList(asps []*ct.AppSpecificPassword, id string) []*ct.AppSpecificPassword {
var out []*ct.AppSpecificPassword
for _, asp := range asps {
if asp.ID != id {
out = append(out, asp)
}
}
return out
}
func encodeAppSpecificPasswords(asps []*appSpecificPassword) []string {
func encodeAppSpecificPasswords(asps []*ct.AppSpecificPassword) []string {
var out []string
for _, asp := range asps {
out = append(out, asp.Encode())
out = append(out, asp.Marshal())
}
return out
}
func getASPInfo(asps []*appSpecificPassword) []*as.AppSpecificPasswordInfo {
func getASPInfo(asps []*ct.AppSpecificPassword) []*as.AppSpecificPasswordInfo {
var out []*as.AppSpecificPasswordInfo
for _, asp := range asps {
out = append(out, &asp.AppSpecificPasswordInfo)
out = append(out, &as.AppSpecificPasswordInfo{
ID: asp.ID,
Service: asp.Service,
Comment: asp.Comment,
})
}
return out
}
func decodeUserEncryptionKeys(values []string) []*as.UserEncryptionKey {
var out []*as.UserEncryptionKey
func decodeUserEncryptionKeys(values []string) []*ct.EncryptedKey {
var out []*ct.EncryptedKey
for _, value := range values {
idx := strings.IndexByte(value, ':')
if idx < 0 {
k, err := ct.UnmarshalEncryptedKey(value)
if err != nil {
continue
}
out = append(out, &as.UserEncryptionKey{
ID: value[:idx],
Key: []byte(value[idx+1:]),
})
out = append(out, k)
}
return out
}
func encodeUserEncryptionKeys(keys []*as.UserEncryptionKey) []string {
func encodeUserEncryptionKeys(keys []*ct.EncryptedKey) []string {
var out []string
for _, key := range keys {
out = append(out, fmt.Sprintf("%s:%s", key.ID, string(key.Key)))
out = append(out, key.Marshal())
}
return out
}
func decodeU2FRegistration(enc string) (*as.U2FRegistration, error) {
var reg u2f.Registration
if err := reg.UnmarshalBinary([]byte(enc)); err != nil {
return nil, err
}
return &as.U2FRegistration{Registration: &reg}, nil
}
func encodeU2FRegistration(r *as.U2FRegistration) string {
// MarshalBinary can't fail, ignore error.
b, _ := r.MarshalBinary() // nolint
return string(b)
}
func decodeU2FRegistrations(encRegs []string) []*as.U2FRegistration {
var out []*as.U2FRegistration
for _, enc := range encRegs {
if r, err := decodeU2FRegistration(enc); err == nil {
out = append(out, r)
if r, err := ct.UnmarshalU2FRegistration(enc); err == nil {
// Mirror ct.U2FRegistration -> as.U2FRegistration.
out = append(out, &as.U2FRegistration{
KeyHandle: r.KeyHandle,
PublicKey: r.PublicKey,
})
}
}
return out
......@@ -118,7 +91,12 @@ func decodeU2FRegistrations(encRegs []string) []*as.U2FRegistration {
func encodeU2FRegistrations(regs []*as.U2FRegistration) []string {
var out []string
for _, r := range regs {
out = append(out, encodeU2FRegistration(r))
// Mirror as.U2FRegistration -> ct.U2FRegistration.
ctr := ct.U2FRegistration{
KeyHandle: r.KeyHandle,
PublicKey: r.PublicKey,
}
out = append(out, ctr.Marshal())
}
return out
}
......@@ -9,6 +9,7 @@ import (
"time"
ldaputil "git.autistici.org/ai3/go-common/ldap"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
"gopkg.in/ldap.v3"
as "git.autistici.org/ai3/accountserver"
......@@ -319,7 +320,7 @@ func (tx *backendTX) DeleteAccountRecoveryHint(ctx context.Context, user *as.Use
return nil
}
func (tx *backendTX) SetUserEncryptionKeys(ctx context.Context, user *as.User, keys []*as.UserEncryptionKey) error {
func (tx *backendTX) SetUserEncryptionKeys(ctx context.Context, user *as.User, keys []*ct.EncryptedKey) error {
encKeys := encodeUserEncryptionKeys(keys)
for _, r := range user.GetResourcesByType(as.ResourceTypeEmail) {
dn, err := tx.backend.resources.GetDN(r.ID)
......@@ -342,16 +343,6 @@ func (tx *backendTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.Us
return nil
}
func excludeASPFromList(asps []*appSpecificPassword, id string) []*appSpecificPassword {
var out []*appSpecificPassword
for _, asp := range asps {
if asp.ID != id {
out = append(out, asp)
}
}
return out
}
func (tx *backendTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, info *as.AppSpecificPasswordInfo, encryptedPassword string) error {
dn := tx.getUserDN(user)
asps := decodeAppSpecificPasswords(tx.readAttributeValues(ctx, dn, aspLDAPAttr))
......
......@@ -10,6 +10,7 @@ import (
as "git.autistici.org/ai3/accountserver"
"git.autistici.org/ai3/accountserver/ldaptest"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
)
const (
......@@ -307,10 +308,10 @@ func TestModel_SetUserEncryptionKeys_Add(t *testing.T) {
defer stop()
tx, _ := b.NewTransaction()
keys := []*as.UserEncryptionKey{
keys := []*ct.EncryptedKey{
{
ID: as.UserEncryptionKeyMainID,
Key: []byte("very secret key"),
ID: as.UserEncryptionKeyMainID,
EncryptedKey: []byte("very secret key"),
},
}
if err := tx.SetUserEncryptionKeys(context.Background(), &user.User, keys); err != nil {
......@@ -326,10 +327,10 @@ func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) {
defer stop()
tx, _ := b.NewTransaction()
keys := []*as.UserEncryptionKey{
keys := []*ct.EncryptedKey{
{
ID: as.UserEncryptionKeyMainID,
Key: []byte("very secret key"),
ID: as.UserEncryptionKeyMainID,
EncryptedKey: []byte("very secret key"),
},
}
if err := tx.SetUserEncryptionKeys(context.Background(), &user.User, keys); err != nil {
......
......@@ -18,6 +18,7 @@ import (
"git.autistici.org/ai3/accountserver/backend"
"git.autistici.org/ai3/accountserver/ldaptest"
"git.autistici.org/ai3/accountserver/server"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
"git.autistici.org/ai3/go-common/pwhash"
"git.autistici.org/ai3/go-common/userenckey"
"git.autistici.org/id/go-sso"
......@@ -199,10 +200,10 @@ func checkUserInvariants(t *testing.T, be as.Backend, username, primaryPassword
return user
}
func keysToBytes(keys []*as.UserEncryptionKey) [][]byte {
func keysToBytes(keys []*ct.EncryptedKey) [][]byte {
var rawKeys [][]byte
for _, k := range keys {
rawKeys = append(rawKeys, k.Key)
rawKeys = append(rawKeys, k.EncryptedKey)
}
return rawKeys
}
package accountserver
import (
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
"git.autistici.org/ai3/go-common/userenckey"
)
// A list of encrypted keys, all copies of the same key but encrypted with
// different passwords.
type encryptedKeyList []*ct.EncryptedKey
func newEncryptionKeys(encryptionPassword string) ([]byte, encryptedKeyList, error) {
pub, priv, err := userenckey.GenerateKey()
if err != nil {
return nil, nil, err
}
encrypted, err := userenckey.Encrypt(priv, []byte(encryptionPassword))
if err != nil {
return nil, nil, err
}
l := encryptedKeyList([]*ct.EncryptedKey{
&ct.EncryptedKey{
ID: UserEncryptionKeyMainID,
EncryptedKey: encrypted,
},
})
return pub, l, nil
}
func keysToBytes(keys []*ct.EncryptedKey) [][]byte {
var rawKeys [][]byte
for _, k := range keys {
rawKeys = append(rawKeys, k.EncryptedKey)
}
return rawKeys
}
func (l encryptedKeyList) add(keyID, unlockPassword, encryptionPassword string) (encryptedKeyList, error) {
decrypted, err := userenckey.Decrypt(keysToBytes(l), []byte(unlockPassword))
if err != nil {
return nil, err
}
encrypted, err := userenckey.Encrypt(decrypted, []byte(encryptionPassword))
if err != nil {
return nil, err
}
l = l.deleteByID(keyID)
return append(l, &ct.EncryptedKey{
ID: keyID,
EncryptedKey: encrypted,
}), nil
}
func (l encryptedKeyList) deleteByID(keyID string) encryptedKeyList {
var out encryptedKeyList
for _, k := range l {
if k.ID != keyID {
out = append(out, k)
}
}
return out
}
// Return the ID for the encrypted key associated with an app-specific
// password.
func aspKeyID(aspID string) string {
return "asp_" + aspID
}
......@@ -6,6 +6,7 @@ import (
"log"
"time"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
"git.autistici.org/id/go-sso"
umdb "git.autistici.org/id/usermetadb"
umdbc "git.autistici.org/id/usermetadb/client"
......@@ -55,7 +56,7 @@ type TX interface {
SetUserPassword(context.Context, *User, string) error
SetAccountRecoveryHint(context.Context, *User, string, string) error
DeleteAccountRecoveryHint(context.Context, *User) error
SetUserEncryptionKeys(context.Context, *User, []*UserEncryptionKey) error
SetUserEncryptionKeys(context.Context, *User, []*ct.EncryptedKey) error
SetUserEncryptionPublicKey(context.Context, *User, []byte) error
SetApplicationSpecificPassword(context.Context, *User, *AppSpecificPasswordInfo, string) error
DeleteApplicationSpecificPassword(context.Context, *User, string) error
......
......@@ -8,8 +8,6 @@ import (
"time"
"git.autistici.org/ai3/go-common/pwhash"
"git.autistici.org/ai3/go-common/userenckey"
"github.com/tstranex/u2f"
)
// Possible values for user status.
......@@ -339,63 +337,6 @@ func (u *RawUser) deleteAllApplicationSpecificPasswords(ctx context.Context, tx
return nil
}
// A list of encrypted keys, all copies of the same key but encrypted with
// different passwords.
type encryptedKeyList []*UserEncryptionKey
func newEncryptionKeys(encryptionPassword string) ([]byte, encryptedKeyList, error) {
pub, priv, err := userenckey.GenerateKey()
if err != nil {
return nil, nil, err
}
encrypted, err := userenckey.Encrypt(priv, []byte(encryptionPassword))
if err != nil {
return nil, nil, err
}
l := encryptedKeyList([]*UserEncryptionKey{
&UserEncryptionKey{
ID: UserEncryptionKeyMainID,
Key: encrypted,
},
})
return pub, l, nil
}
func keysToBytes(keys []*UserEncryptionKey) [][]byte {
var rawKeys [][]byte
for _, k := range keys {
rawKeys = append(rawKeys, k.Key)
}
return rawKeys
}
func (l encryptedKeyList) add(keyID, unlockPassword, encryptionPassword string) (encryptedKeyList, error) {
decrypted, err := userenckey.Decrypt(keysToBytes(l), []byte(unlockPassword))
if err != nil {
return nil, err
}
encrypted, err := userenckey.Encrypt(decrypted, []byte(encryptionPassword))
if err != nil {
return nil, err
}
l = l.deleteByID(keyID)
return append(l, &UserEncryptionKey{
ID: keyID,
Key: encrypted,
}), nil
}
func (l encryptedKeyList) deleteByID(keyID string) encryptedKeyList {
var out encryptedKeyList
for _, k := range l {
if k.ID != keyID {
out = append(out, k)
}
}
return out
}
// AppSpecificPasswordInfo stores public information about an
// app-specific password.
type AppSpecificPasswordInfo struct {
......@@ -404,10 +345,6 @@ type AppSpecificPasswordInfo struct {
Comment string `json:"comment"`
}
func aspKeyID(aspID string) string {
return "asp_" + aspID
}
// Well-known user encryption key types, corresponding to primary and
// secondary passwords.
const (
......@@ -415,13 +352,6 @@ const (
UserEncryptionKeyRecoveryID = "recovery"
)
// UserEncryptionKey stores a password-encrypted secret key for the
// user's encrypted storage.
type UserEncryptionKey struct {
ID string `json:"id"`
Key []byte `json:"key"`
}
// Resource types.
const (
ResourceTypeEmail = "email"
......@@ -659,32 +589,12 @@ func getHostingDir(path, siteRoot string) string {
return path
}
// U2FRegistration is just a thin wrapper for u2f.Registration that
// supports serialization and deserialization to JSON. The serialized
// format is simply the raw registration data (base64-encoded for
// transport).
// U2FRegistration stores information on a single U2F device registration.
//
// This is a mirror of compositetypes.U2FRegistration, but it is
// duplicated since this is part of our public interface, and
// compositetypes is a detail of the LDAP backend implementation.
type U2FRegistration struct {
*u2f.Registration
}
// MarshalJSON implements the json.Marshaler interface.
func (r *U2FRegistration) MarshalJSON() ([]byte, error) {
data, err := r.Registration.MarshalBinary()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (r *U2FRegistration) UnmarshalJSON(data []byte) error {
if data == nil {
return nil
}
var b []byte
if err := json.Unmarshal(data, &b); err != nil {
return err
}
r.Registration = new(u2f.Registration)
return r.Registration.UnmarshalBinary(b)
KeyHandle []byte `json:"key_handle"`
PublicKey []byte `json:"public_key"`
}
package accountserver
import (
"encoding/json"
"testing"
)
var testReg = `"BQRymreLJraApU27oZ0dpjFW7VsjwfRUwblUfhEPUr9zOiryBSvCmVAL0pqJAMuV70qu6U0t70hMo0cUxec0evdKQFlPUJcS2P0HgoyNq8JHGQ5LjF26gbYRDSkCmdqxjP/i8lPSTiFqAucnLhAhQaAw7khfLJ3nszzNNCrgExmE6ocwggGHMIIBLqADAgECAgkAmb7osQyi7BwwCQYHKoZIzj0EATAhMR8wHQYDVQQDDBZZdWJpY28gVTJGIFNvZnQgRGV2aWNlMB4XDTEzMDcxNzE0MjEwM1oXDTE2MDcxNjE0MjEwM1owITEfMB0GA1UEAwwWWXViaWNvIFUyRiBTb2Z0IERldmljZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDvhl91zfpg9n7DeCedcQ8gGXUnemiXoi+JEAxz+EIhkVsMPAyzhtJZ4V3CqMZ+MOUgICt2aMxacMX9cIa8dgS2jUDBOMB0GA1UdDgQWBBQNqL+TV04iaO6mS5tjGE6ShfexnjAfBgNVHSMEGDAWgBQNqL+TV04iaO6mS5tjGE6ShfexnjAMBgNVHRMEBTADAQH/MAkGByqGSM49BAEDSAAwRQIgXJWZdbvOWdhVaG7IJtn44o21Kmi8EHsDk4cAfnZ0r38CIQD6ZPi3Pl4lXxbY7BXFyrpkiOvCpdyNdLLYbSTbvIBQOTBGAiEA2oN5nlqtnpgV2UQNv2bIa29vyoTB+oBj9xkaGU8Ozm8CIQDueoTlbdFV7sLLMpj5jwpqktwl6UiPZnmQtpUyYmc21w=="`
func TestU2FRegistration_Unmarshal(t *testing.T) {
var reg U2FRegistration
if err := json.Unmarshal([]byte(testReg), &reg); err != nil {
t.Fatal(err)
}
}
// Package compositetypes provides Go types for the composite values
// stored in our LDAP database, so that various authentication
// packages can agree on their serialized representation.
//
// These are normally 1-to-many associations that are wrapped into
// repeated LDAP attributes instead of separate nested objects, for
// simplicity and latency reasons.
//
// Whenever there is an 'id' field, it's a unique (per-user)
// identifier used to recognize a specific entry on modify/delete.
//
// The serialized values can be arbitrary []byte sequences (the LDAP
// schema should specify the right types for the associated
// attributes).
//
package compositetypes
import (
"crypto/elliptic"
"errors"
"strings"
"github.com/tstranex/u2f"
)
// AppSpecificPassword stores information on an application-specific
// password.
//
// Serialized as colon-separated fields with the format:
//
// id:service:encrypted_password:comment
//
// Where 'comment' is free-form and can contain colons, no escaping is
// performed.
type AppSpecificPassword struct {
ID string `json:"id"`
Service string `json:"service"`
EncryptedPassword string `json:"encrypted_password"`
Comment string `json:"comment"`
}
// Marshal returns the serialized format.
func (p *AppSpecificPassword) Marshal() string {
return strings.Join([]string{
p.ID,
p.Service,
p.EncryptedPassword,
p.Comment,
}, ":")
}
// UnmarshalAppSpecificPassword parses a serialized representation of
// an AppSpecificPassword.
func UnmarshalAppSpecificPassword(s string) (*AppSpecificPassword, error) {
parts := strings.SplitN(s, ":", 4)
if len(parts) != 4 {
return nil, errors.New("badly encoded app-specific password")
}
return &AppSpecificPassword{
ID: parts[0],
Service: parts[1],
EncryptedPassword: parts[2],
Comment: parts[3],
}, nil
}
// EncryptedKey stores a password-encrypted secret key.
//
// Serialized as colon-separated fields with the format:
//
// id:encrypted_key
//
// The encrypted key is stored as a raw, unencoded byte sequence.
type EncryptedKey struct {
ID string `json:"id"`
EncryptedKey []byte `json:"encrypted_key"`
}
// Marshal returns the serialized format.
func (k *EncryptedKey) Marshal() string {
var b []byte
b = append(b, []byte(k.ID)...)
b = append(b, ':')
b = append(b, k.EncryptedKey...)
return string(b)
}
// UnmarshalEncryptedKey parses the serialized representation of an
// EncryptedKey.
func UnmarshalEncryptedKey(s string) (*EncryptedKey, error) {
idx := strings.IndexByte(s, ':')
if idx < 0 {
return nil, errors.New("badly encoded key")
}
return &EncryptedKey{
ID: s[:idx],
EncryptedKey: []byte(s[idx+1:]),
}, nil
}
// U2FRegistration stores information on a single U2F device
// registration.
//
// The serialized format follows part of the U2F standard and just
// stores 64 bytes of the public key immediately followed by the key
// handle data, with no encoding.
//
// The data in U2FRegistration is still encoded, but it can be turned
// into a usable form (github.com/tstranex/u2f.Registration) later.
type U2FRegistration struct {
KeyHandle []byte `json:"key_handle"`
PublicKey []byte `json:"public_key"`
}
// Marshal returns the serialized format.
func (r *U2FRegistration) Marshal() string {
var b []byte
b = append(b, r.PublicKey...)
b = append(b, r.KeyHandle...)
return string(b)
}
// UnmarshalU2FRegistration parses a U2FRegistration from its serialized format.
func UnmarshalU2FRegistration(s string) (*U2FRegistration, error) {
if len(s) < 64 {
return nil, errors.New("badly encoded u2f registration")
}
b := []byte(s)
return &U2FRegistration{
PublicKey: b[:64],
KeyHandle: b[64:],
}, nil
}
// Decode returns a u2f.Registration object with the decoded public
// key ready for use in verification.
func (r *U2FRegistration) Decode() (*u2f.Registration, error) {
x, y := elliptic.Unmarshal(elliptic.P256(), r.PublicKey)
if x == nil {
return nil, errors.New("invalid public key")
}
var reg u2f.Registration
reg.PubKey.Curve = elliptic.P256()
reg.PubKey.X = x
reg.PubKey.Y = y
reg.KeyHandle = r.KeyHandle
return &reg, nil
}
// NewU2FRegistrationFromData creates a U2FRegistration from a
// u2f.Registration object.
func NewU2FRegistrationFromData(reg *u2f.Registration) *U2FRegistration {
pk := elliptic.Marshal(reg.PubKey.Curve, reg.PubKey.X, reg.PubKey.Y)
return &U2FRegistration{
PublicKey: pk,
KeyHandle: reg.KeyHandle,
}
}
......@@ -20,6 +20,12 @@
"revision": "2934fd63c275d37b0fe60afabb484a251662bd49",