Add PoC CardDAV server

This commit is contained in:
emersion 2017-09-03 20:11:01 +02:00
parent f7c8ce8d9f
commit 937242e3ef
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
4 changed files with 227 additions and 19 deletions

88
carddav/carddav.go Normal file
View File

@ -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})
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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) {