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 (
"bytes"
"errors"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"strconv"
"sync"
"time"
"github.com/emersion/go-vcard"
"github.com/emersion/go-webdav/carddav"
"github.com/emersion/hydroxide/protonmail"
"golang.org/x/crypto/openpgp"
"log"
)
type contextKey string
const ClientContextKey = contextKey("client")
func formatCard(card vcard.Card) (*protonmail.ContactImport, error) {
// TODO: sign/encrypt stuff
var (
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
if err := vcard.NewEncoder(&b).Encode(card); err != nil {
return nil, err
func formatCard(card vcard.Card, privateKey *openpgp.Entity) (*protonmail.ContactImport, error) {
vcard.ToV4(card)
// Add groups to emails
i := 1
for _, email := range card[vcard.FieldEmail] {
if email.Group == "" {
email.Group = strconv.Itoa(i)
i++
}
}
return &protonmail.ContactImport{
Cards: []*protonmail.ContactCard{
{Data: b.String()},
},
}, nil
toEncrypt := card
toSign := make(vcard.Card)
for _, k := range signedCardProps {
if fields, ok := toEncrypt[k]; ok {
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 {
@ -62,7 +112,7 @@ func (fi *addressFileInfo) Sys() interface{} {
}
type addressObject struct {
c *protonmail.Client
ab *addressBook
contact *protonmail.Contact
}
@ -78,19 +128,25 @@ func (ao *addressObject) Card() (vcard.Card, error) {
card := make(vcard.Card)
for _, c := range ao.contact.Cards {
if c.Type.Encrypted() {
// TODO: decrypt
continue
}
if c.Type.Signed() {
// TODO: check signature
md, err := c.Read(ao.ab.privateKeys)
if err != nil {
return nil, err
}
decoded, err := vcard.NewDecoder(strings.NewReader(c.Data)).Decode()
decoded, err := vcard.NewDecoder(md.UnverifiedBody).Decode()
if err != nil {
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 _, f := range fields {
card.Add(k, f)
@ -102,12 +158,12 @@ func (ao *addressObject) 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 {
return err
}
contact, err := ao.c.UpdateContact(ao.contact.ID, contactImport)
contact, err := ao.ab.c.UpdateContact(ao.contact.ID, contactImport)
if err != nil {
return err
}
@ -122,6 +178,7 @@ type addressBook struct {
cache map[string]*addressObject
locker sync.Mutex
total int
privateKeys openpgp.EntityList
}
func (ab *addressBook) Info() (*carddav.AddressBookInfo, error) {
@ -177,7 +234,7 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) {
for _, contact := range contacts {
if _, ok := ab.addressObject(contact.ID); !ok {
ab.cacheAddressObject(&addressObject{
c: ab.c,
ab: ab,
contact: contact,
})
}
@ -200,7 +257,7 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) {
ao, ok := ab.addressObject(contact.ID)
if !ok {
ao = &addressObject{
c: ab.c,
ab: ab,
contact: &protonmail.Contact{ID: contact.ID},
}
ab.cacheAddressObject(ao)
@ -233,7 +290,7 @@ func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error
}
ao := &addressObject{
c: ab.c,
ab: ab,
contact: contact,
}
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) {
contactImport, err := formatCard(card)
contactImport, err := formatCard(card, ab.privateKeys[0])
if err != nil {
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
ao := &addressObject{
c: ab.c,
ab: ab,
contact: contact,
}
ab.cacheAddressObject(ao)
@ -282,7 +339,7 @@ func (ab *addressBook) receiveEvents(events <-chan *protonmail.Event) {
fallthrough
case protonmail.EventUpdate:
ab.cache[eventContact.ID] = &addressObject{
c: ab.c,
ab: ab,
contact: eventContact.Contact,
}
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{
c: c,
cache: make(map[string]*addressObject),
total: -1,
privateKeys: privateKeys,
}
if events != nil {

View File

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