carddav: properly sign, encrypt, decrypt cards

This commit is contained in:
emersion 2017-09-13 18:53:40 +02:00
parent a3c439da70
commit 43097588a9
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
2 changed files with 99 additions and 34 deletions

View File

@ -3,34 +3,84 @@ package carddav
import ( import (
"bytes" "bytes"
"errors" "errors"
"io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"strings" "strconv"
"sync" "sync"
"time" "time"
"github.com/emersion/go-vcard" "github.com/emersion/go-vcard"
"github.com/emersion/go-webdav/carddav" "github.com/emersion/go-webdav/carddav"
"github.com/emersion/hydroxide/protonmail" "github.com/emersion/hydroxide/protonmail"
"golang.org/x/crypto/openpgp"
"log"
) )
type contextKey string type contextKey string
const ClientContextKey = contextKey("client") const ClientContextKey = contextKey("client")
func formatCard(card vcard.Card) (*protonmail.ContactImport, error) { var (
// TODO: sign/encrypt stuff cleartextCardProps = []string{vcard.FieldVersion, vcard.FieldProductID, "X-PM-LABEL", "X-PM-GROUP"}
signedCardProps = []string{vcard.FieldVersion, vcard.FieldProductID, vcard.FieldFormattedName, vcard.FieldUID, vcard.FieldEmail}
)
var b bytes.Buffer func formatCard(card vcard.Card, privateKey *openpgp.Entity) (*protonmail.ContactImport, error) {
if err := vcard.NewEncoder(&b).Encode(card); err != nil { vcard.ToV4(card)
return nil, err
// Add groups to emails
i := 1
for _, email := range card[vcard.FieldEmail] {
if email.Group == "" {
email.Group = strconv.Itoa(i)
i++
}
} }
return &protonmail.ContactImport{ toEncrypt := card
Cards: []*protonmail.ContactCard{ toSign := make(vcard.Card)
{Data: b.String()}, for _, k := range signedCardProps {
}, if fields, ok := toEncrypt[k]; ok {
}, nil toSign[k] = fields
if k != vcard.FieldVersion {
delete(toEncrypt, k)
}
}
}
var contactImport protonmail.ContactImport
var b bytes.Buffer
if len(toSign) > 0 {
if err := vcard.NewEncoder(&b).Encode(toSign); err != nil {
return nil, err
}
signed, err := protonmail.NewSignedContactCard(&b, privateKey)
if err != nil {
return nil, err
}
contactImport.Cards = append(contactImport.Cards, signed)
b.Reset()
}
if len(toEncrypt) > 0 {
if err := vcard.NewEncoder(&b).Encode(toEncrypt); err != nil {
return nil, err
}
to := []*openpgp.Entity{privateKey}
encrypted, err := protonmail.NewEncryptedContactCard(&b, to, privateKey)
if err != nil {
return nil, err
}
log.Println(encrypted)
contactImport.Cards = append(contactImport.Cards, encrypted)
b.Reset()
}
return &contactImport, nil
} }
type addressFileInfo struct { type addressFileInfo struct {
@ -62,7 +112,7 @@ func (fi *addressFileInfo) Sys() interface{} {
} }
type addressObject struct { type addressObject struct {
c *protonmail.Client ab *addressBook
contact *protonmail.Contact contact *protonmail.Contact
} }
@ -78,19 +128,25 @@ func (ao *addressObject) Card() (vcard.Card, error) {
card := make(vcard.Card) card := make(vcard.Card)
for _, c := range ao.contact.Cards { for _, c := range ao.contact.Cards {
if c.Type.Encrypted() { md, err := c.Read(ao.ab.privateKeys)
// TODO: decrypt if err != nil {
continue return nil, err
}
if c.Type.Signed() {
// TODO: check signature
} }
decoded, err := vcard.NewDecoder(strings.NewReader(c.Data)).Decode() decoded, err := vcard.NewDecoder(md.UnverifiedBody).Decode()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// The signature can be checked only if md.UnverifiedBody is consumed until
// EOF
if _, err := io.Copy(ioutil.Discard, md.UnverifiedBody); err != nil {
return nil, err
}
if err := md.SignatureError; err != nil {
return nil, err
}
for k, fields := range decoded { for k, fields := range decoded {
for _, f := range fields { for _, f := range fields {
card.Add(k, f) card.Add(k, f)
@ -102,12 +158,12 @@ func (ao *addressObject) Card() (vcard.Card, error) {
} }
func (ao *addressObject) SetCard(card vcard.Card) error { func (ao *addressObject) SetCard(card vcard.Card) error {
contactImport, err := formatCard(card) contactImport, err := formatCard(card, ao.ab.privateKeys[0])
if err != nil { if err != nil {
return err return err
} }
contact, err := ao.c.UpdateContact(ao.contact.ID, contactImport) contact, err := ao.ab.c.UpdateContact(ao.contact.ID, contactImport)
if err != nil { if err != nil {
return err return err
} }
@ -122,6 +178,7 @@ type addressBook struct {
cache map[string]*addressObject cache map[string]*addressObject
locker sync.Mutex locker sync.Mutex
total int total int
privateKeys openpgp.EntityList
} }
func (ab *addressBook) Info() (*carddav.AddressBookInfo, error) { func (ab *addressBook) Info() (*carddav.AddressBookInfo, error) {
@ -177,7 +234,7 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) {
for _, contact := range contacts { for _, contact := range contacts {
if _, ok := ab.addressObject(contact.ID); !ok { if _, ok := ab.addressObject(contact.ID); !ok {
ab.cacheAddressObject(&addressObject{ ab.cacheAddressObject(&addressObject{
c: ab.c, ab: ab,
contact: contact, contact: contact,
}) })
} }
@ -200,7 +257,7 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) {
ao, ok := ab.addressObject(contact.ID) ao, ok := ab.addressObject(contact.ID)
if !ok { if !ok {
ao = &addressObject{ ao = &addressObject{
c: ab.c, ab: ab,
contact: &protonmail.Contact{ID: contact.ID}, contact: &protonmail.Contact{ID: contact.ID},
} }
ab.cacheAddressObject(ao) ab.cacheAddressObject(ao)
@ -233,7 +290,7 @@ func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error
} }
ao := &addressObject{ ao := &addressObject{
c: ab.c, ab: ab,
contact: contact, contact: contact,
} }
ab.cacheAddressObject(ao) ab.cacheAddressObject(ao)
@ -241,7 +298,7 @@ func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error
} }
func (ab *addressBook) CreateAddressObject(card vcard.Card) (carddav.AddressObject, error) { func (ab *addressBook) CreateAddressObject(card vcard.Card) (carddav.AddressObject, error) {
contactImport, err := formatCard(card) contactImport, err := formatCard(card, ab.privateKeys[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -261,7 +318,7 @@ func (ab *addressBook) CreateAddressObject(card vcard.Card) (carddav.AddressObje
contact.Cards = contactImport.Cards // Not returned by the server contact.Cards = contactImport.Cards // Not returned by the server
ao := &addressObject{ ao := &addressObject{
c: ab.c, ab: ab,
contact: contact, contact: contact,
} }
ab.cacheAddressObject(ao) ab.cacheAddressObject(ao)
@ -282,7 +339,7 @@ func (ab *addressBook) receiveEvents(events <-chan *protonmail.Event) {
fallthrough fallthrough
case protonmail.EventUpdate: case protonmail.EventUpdate:
ab.cache[eventContact.ID] = &addressObject{ ab.cache[eventContact.ID] = &addressObject{
c: ab.c, ab: ab,
contact: eventContact.Contact, contact: eventContact.Contact,
} }
case protonmail.EventDelete: case protonmail.EventDelete:
@ -295,11 +352,16 @@ func (ab *addressBook) receiveEvents(events <-chan *protonmail.Event) {
} }
} }
func NewHandler(c *protonmail.Client, events <-chan *protonmail.Event) http.Handler { func NewHandler(c *protonmail.Client, privateKeys openpgp.EntityList, events <-chan *protonmail.Event) http.Handler {
if len(privateKeys) == 0 {
panic("hydroxide/carddav: no private key available")
}
ab := &addressBook{ ab := &addressBook{
c: c, c: c,
cache: make(map[string]*addressObject), cache: make(map[string]*addressObject),
total: -1, total: -1,
privateKeys: privateKeys,
} }
if events != nil { if events != nil {

View File

@ -18,6 +18,7 @@ import (
"github.com/emersion/hydroxide/protonmail" "github.com/emersion/hydroxide/protonmail"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/openpgp"
) )
const authFile = "auth.json" const authFile = "auth.json"
@ -111,16 +112,15 @@ func newClient() *protonmail.Client {
} }
} }
func authenticate(c *protonmail.Client, cachedAuth *cachedAuth) error { func authenticate(c *protonmail.Client, cachedAuth *cachedAuth) (openpgp.EntityList, error) {
auth, err := c.AuthRefresh(&cachedAuth.Auth) auth, err := c.AuthRefresh(&cachedAuth.Auth)
if err != nil { if err != nil {
// TODO: handle expired token, re-authenticate // TODO: handle expired token, re-authenticate
return err return nil, err
} }
cachedAuth.Auth = *auth cachedAuth.Auth = *auth
_, err = c.Unlock(auth, cachedAuth.MailboxPassword) return c.Unlock(auth, cachedAuth.MailboxPassword)
return err
} }
func receiveEvents(c *protonmail.Client, last string, ch chan<- *protonmail.Event) { func receiveEvents(c *protonmail.Client, last string, ch chan<- *protonmail.Event) {
@ -146,6 +146,7 @@ func receiveEvents(c *protonmail.Client, last string, ch chan<- *protonmail.Even
type session struct { type session struct {
h http.Handler h http.Handler
hashedSecretKey []byte hashedSecretKey []byte
privateKeys openpgp.EntityList
} }
func main() { func main() {
@ -298,7 +299,8 @@ func main() {
// authenticate updates cachedAuth with the new refresh token // authenticate updates cachedAuth with the new refresh token
c := newClient() c := newClient()
if err := authenticate(c, &cachedAuth); err != nil { privateKeys, err := authenticate(c, &cachedAuth)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError) resp.WriteHeader(http.StatusInternalServerError)
log.Printf("Cannot authenticate %q: %v", username, err) log.Printf("Cannot authenticate %q: %v", username, err)
return return
@ -319,11 +321,12 @@ func main() {
events := make(chan *protonmail.Event) events := make(chan *protonmail.Event)
go receiveEvents(c, cachedAuth.EventID, events) go receiveEvents(c, cachedAuth.EventID, events)
h = carddav.NewHandler(c, events) h = carddav.NewHandler(c, privateKeys, events)
sessions[username] = &session{ sessions[username] = &session{
h: h, h: h,
hashedSecretKey: hashed, hashedSecretKey: hashed,
privateKeys: privateKeys,
} }
} }