carddav: properly sign, encrypt, decrypt cards
This commit is contained in:
parent
a3c439da70
commit
43097588a9
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue