From 937242e3ef05f6ecb18d0baa05e3f5c7fa6338c6 Mon Sep 17 00:00:00 2001 From: emersion Date: Sun, 3 Sep 2017 20:11:01 +0200 Subject: [PATCH] Add PoC CardDAV server --- carddav/carddav.go | 88 ++++++++++++++++++++++++++++++++ cmd/hydroxide/hydroxide.go | 12 +++++ protonmail/auth.go | 45 +++++++++++++++-- protonmail/contacts.go | 101 +++++++++++++++++++++++++++++++------ 4 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 carddav/carddav.go diff --git a/carddav/carddav.go b/carddav/carddav.go new file mode 100644 index 0000000..056eb92 --- /dev/null +++ b/carddav/carddav.go @@ -0,0 +1,88 @@ +package carddav + +import ( + "net/http" + "strings" + + "github.com/emersion/hydroxide/protonmail" + "github.com/emersion/go-vcard" + "github.com/emersion/go-webdav/carddav" + + "log" +) + +type addressObject struct { + c *protonmail.Client + contact *protonmail.ContactExport +} + +func (ao *addressObject) ID() string { + return ao.contact.ID +} + +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 + } + + decoded, err := vcard.NewDecoder(strings.NewReader(c.Data)).Decode() + if err != nil { + return nil, err + } + + for k, fields := range decoded { + for _, f := range fields { + card.Add(k, f) + } + } + } + + return card, nil +} + +type addressBook struct { + c *protonmail.Client +} + +func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) { + // TODO: cache this + // TODO: paging support + _, contacts, err := ab.c.ListContactsExport(0, 0) +log.Println(contacts, err) + if err != nil { + return nil, err + } + + aos := make([]carddav.AddressObject, len(contacts)) + for i, contact := range contacts { + aos[i] = &addressObject{c: ab.c, contact: contact} + } + + return aos, nil +} + +func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error) { + contact, err := ab.c.GetContact(id) + if err != nil { + return nil, err + } + + return &addressObject{ + c: ab.c, + contact: &protonmail.ContactExport{ + ID: contact.ID, + Cards: contact.Cards, + }, + }, nil +} + +func NewHandler(c *protonmail.Client) http.Handler { + return carddav.NewHandler(&addressBook{c}) +} diff --git a/cmd/hydroxide/hydroxide.go b/cmd/hydroxide/hydroxide.go index 9b7e553..0e5d38a 100644 --- a/cmd/hydroxide/hydroxide.go +++ b/cmd/hydroxide/hydroxide.go @@ -5,8 +5,10 @@ import ( "encoding/json" "fmt" "log" + "net/http" "os" + "github.com/emersion/hydroxide/carddav" "github.com/emersion/hydroxide/protonmail" ) @@ -99,4 +101,14 @@ func main() { if err != nil { log.Fatal(err) } + + h := carddav.NewHandler(c) + + s := &http.Server{ + Addr: "127.0.0.1:8080", + Handler: h, + } + + log.Println("Starting server at", s.Addr) + log.Fatal(s.ListenAndServe()) } diff --git a/protonmail/auth.go b/protonmail/auth.go index 83ae8f3..a4833cb 100644 --- a/protonmail/auth.go +++ b/protonmail/auth.go @@ -3,11 +3,14 @@ package protonmail import ( "encoding/base64" "errors" + "io/ioutil" "net/http" "strings" "time" "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" ) type authInfoReq struct { @@ -204,6 +207,8 @@ func (c *Client) Unlock(auth *Auth, passphrase string) (openpgp.EntityList, erro } } + // Read private keys and unlock them + keyRing, err := openpgp.ReadArmoredKeyRing(strings.NewReader(auth.privateKey)) if err != nil { return nil, err @@ -213,13 +218,47 @@ func (c *Client) Unlock(auth *Auth, passphrase string) (openpgp.EntityList, erro } for _, e := range keyRing { - if err := e.PrivateKey.Decrypt(passphraseBytes); err != nil { - return nil, err + var privateKeys []*packet.PrivateKey + + // e.PrivateKey is a signing key + if e.PrivateKey != nil { + privateKeys = append(privateKeys, e.PrivateKey) + } + + // e.Subkeys are encryption keys + for _, subkey := range e.Subkeys { + if subkey.PrivateKey != nil { + privateKeys = append(privateKeys, subkey.PrivateKey) + } + } + + for _, priv := range privateKeys { + if err := priv.Decrypt(passphraseBytes); err != nil { + return nil, err + } } } + // Decrypt access token + + block, err := armor.Decode(strings.NewReader(auth.accessToken)) + if err != nil { + return nil, err + } + + msg, err := openpgp.ReadMessage(block.Body, keyRing, nil, nil) + if err != nil { + return nil, err + } + + // TODO: maybe check signature + accessTokenBytes, err := ioutil.ReadAll(msg.UnverifiedBody) + if err != nil { + return nil, err + } + c.uid = auth.UID - c.accessToken = auth.accessToken + c.accessToken = string(accessTokenBytes) c.keyRing = keyRing return keyRing, nil } diff --git a/protonmail/contacts.go b/protonmail/contacts.go index 9d68fbd..4187430 100644 --- a/protonmail/contacts.go +++ b/protonmail/contacts.go @@ -2,37 +2,75 @@ package protonmail import ( "net/http" + "net/url" + "strconv" ) type Contact struct { ID string Name string + UID string + Size int + CreateTime int + ModifyTime int LabelIDs []string - Emails []*ContactEmail - Data []*ContactData + // Not when using ListContacts + ContactEmails []*ContactEmail + Cards []*ContactCard } +type ContactEmailDefaults int + type ContactEmail struct { ID string - Name string Email string - Type string - Encrypt int + Type []string + Defaults ContactEmailDefaults Order int ContactID string LabelIDs []string + + // Only when using ListContactsEmails + Name string } -type ContactDataType int +type ContactCardType int const ( - ContactDataEncrypted ContactDataType = 1 + ContactCardCleartext ContactCardType = iota + ContactCardEncrypted + ContactCardSigned + ContactCardEncryptedAndSigned ) -type ContactData struct { - Type ContactDataType - Data string +func (t ContactCardType) Signed() bool { + switch t { + case ContactCardSigned, ContactCardEncryptedAndSigned: + return true + default: + return false + } +} + +func (t ContactCardType) Encrypted() bool { + switch t { + case ContactCardEncrypted, ContactCardEncryptedAndSigned: + return true + default: + return false + } +} + +type ContactCard struct { + Type ContactCardType + Data string + Signature string +} + +type ContactExport struct { + ID string + Cards []*ContactCard } func (c *Client) ListContacts() ([]*Contact, error) { @@ -52,21 +90,52 @@ func (c *Client) ListContacts() ([]*Contact, error) { return respData.Contacts, nil } -func (c *Client) ListContactsEmails() ([]*ContactEmail, error) { - req, err := c.newRequest(http.MethodGet, "/contacts/emails", nil) +func (c *Client) ListContactsEmails(page, pageSize int) (total int, emails []*ContactEmail, err error) { + v := url.Values{} + v.Set("Page", strconv.Itoa(page)) + if pageSize > 0 { + v.Set("PageSize", strconv.Itoa(pageSize)) + } + + req, err := c.newRequest(http.MethodGet, "/contacts/emails?"+v.Encode(), nil) if err != nil { - return nil, err + return 0, nil, err } var respData struct { resp - Contacts []*ContactEmail + ContactEmails []*ContactEmail + Total int } if err := c.doJSON(req, &respData); err != nil { - return nil, err + return 0, nil, err } - return respData.Contacts, nil + return respData.Total, respData.ContactEmails, nil +} + +func (c *Client) ListContactsExport(page, pageSize int) (total int, contacts []*ContactExport, err error) { + v := url.Values{} + v.Set("Page", strconv.Itoa(page)) + if pageSize > 0 { + v.Set("PageSize", strconv.Itoa(pageSize)) + } + + req, err := c.newRequest(http.MethodGet, "/contacts/export?"+v.Encode(), nil) + if err != nil { + return 0, nil, err + } + + var respData struct { + resp + Contacts []*ContactExport + Total int + } + if err := c.doJSON(req, &respData); err != nil { + return 0, nil, err + } + + return respData.Total, respData.Contacts, nil } func (c *Client) GetContact(id string) (*Contact, error) {