mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91dcb2f773 | |||
| c676c732ab | |||
| 444f2d8a12 | |||
| f10da3c7f0 | |||
| b8dd9f82bd | |||
| 1157e60972 | |||
| e9e4d8c725 | |||
| 186fa24106 | |||
| 63780b7b8d | |||
| e3e4769d78 | |||
| b2e9c4e4e9 | |||
| db4cb36538 | |||
| 984864553e | |||
| 2707a5627c | |||
| 8e0d6d41a6 | |||
| 2b76a45e17 | |||
| fce5990d19 |
15
Changelog.md
15
Changelog.md
@ -2,6 +2,21 @@
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Bridge 1.8.9] James
|
||||
|
||||
### Fixed
|
||||
* GODT-1263: Fix crash on invalid or empty header.
|
||||
* GODT-1235: Fix 401 response error handling.
|
||||
* GODT-1261: Fix building messages with long key.
|
||||
* Other: use windows-compatible filename when dumping message in QA builds.
|
||||
|
||||
|
||||
## [Bridge 1.8.8] James
|
||||
|
||||
### Changed
|
||||
* GODT-1234 Set attachment name 'message.eml' for `message/rfc822` attachments.
|
||||
|
||||
|
||||
## [Bridge 1.8.7] James
|
||||
|
||||
### Changed
|
||||
|
||||
2
Makefile
2
Makefile
@ -10,7 +10,7 @@ TARGET_OS?=${GOOS}
|
||||
.PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=1.8.7+git
|
||||
BRIDGE_APP_VERSION?=1.8.9+git
|
||||
IE_APP_VERSION?=1.3.3+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
SRC_ICO:=logo.ico
|
||||
|
||||
@ -48,7 +48,7 @@ func dumpMessageData(b []byte, subject string) {
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(
|
||||
filepath.Join(path, fmt.Sprintf("%v-%v.eml", subject, time.Now().Format(time.RFC3339Nano))),
|
||||
filepath.Join(path, fmt.Sprintf("%v-%v.eml", subject, time.Now().Unix())),
|
||||
b,
|
||||
0600,
|
||||
); err != nil {
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
@ -228,21 +229,28 @@ func (s *Store) Get(userID string) (creds *Credentials, err error) {
|
||||
return s.get(userID)
|
||||
}
|
||||
|
||||
func (s *Store) get(userID string) (creds *Credentials, err error) {
|
||||
func (s *Store) get(userID string) (*Credentials, error) {
|
||||
log := log.WithField("user", userID)
|
||||
|
||||
_, secret, err := s.secrets.Get(userID)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Could not get credentials from native keychain")
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if secret == "" {
|
||||
return nil, errors.New("secret is empty")
|
||||
}
|
||||
|
||||
credentials := &Credentials{UserID: userID}
|
||||
if err = credentials.Unmarshal(secret); err != nil {
|
||||
err = fmt.Errorf("backend/credentials: malformed secret: %v", err)
|
||||
_ = s.secrets.Delete(userID)
|
||||
log.WithError(err).Error("Could not unmarshal secret")
|
||||
return
|
||||
|
||||
if err := credentials.Unmarshal(secret); err != nil {
|
||||
log.WithError(fmt.Errorf("malformed secret: %w", err)).Error("Could not unmarshal secret")
|
||||
|
||||
if err := s.secrets.Delete(userID); err != nil {
|
||||
log.WithError(err).Error("Failed to remove malformed secret")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return credentials, nil
|
||||
|
||||
@ -279,26 +279,6 @@ func (u *User) GetStoreAddresses() []string {
|
||||
return u.creds.EmailList()
|
||||
}
|
||||
|
||||
// getStoreAddresses returns a user's used addresses (with the original address in first place).
|
||||
func (u *User) getStoreAddresses() []string { // nolint[unused]
|
||||
addrInfo, err := u.store.GetAddressInfo()
|
||||
if err != nil {
|
||||
u.log.WithError(err).Error("Failed getting address info from store")
|
||||
return nil
|
||||
}
|
||||
|
||||
addresses := []string{}
|
||||
for _, addr := range addrInfo {
|
||||
addresses = append(addresses, addr.Address)
|
||||
}
|
||||
|
||||
if u.IsCombinedAddressMode() {
|
||||
return addresses[:1]
|
||||
}
|
||||
|
||||
return addresses
|
||||
}
|
||||
|
||||
// GetAddresses returns list of all addresses.
|
||||
func (u *User) GetAddresses() []string {
|
||||
u.lock.RLock()
|
||||
|
||||
@ -347,6 +347,12 @@ func writeMultipartEncryptedRFC822(header message.Header, body []byte) ([]byte,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If parsed header is empty then either it is malformed or it is missing.
|
||||
// Anyway message could not be considered multipart/mixed anymore since there will be no boundary.
|
||||
if bodyHeader.Len() == 0 {
|
||||
header.Del("Content-Type")
|
||||
}
|
||||
|
||||
entFields := bodyHeader.Fields()
|
||||
|
||||
for entFields.Next() {
|
||||
@ -480,7 +486,7 @@ func getAttachmentPartHeader(att *pmapi.Attachment) message.Header {
|
||||
hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)})
|
||||
|
||||
// Use base64 for all attachments except embedded RFC822 messages.
|
||||
if att.MIMEType != "message/rfc822" {
|
||||
if att.MIMEType != rfc822Message {
|
||||
hdr.Set("Content-Transfer-Encoding", "base64")
|
||||
} else {
|
||||
hdr.Del("Content-Transfer-Encoding")
|
||||
@ -494,7 +500,10 @@ func toMessageHeader(hdr mail.Header) message.Header {
|
||||
|
||||
for key, val := range hdr {
|
||||
for _, val := range val {
|
||||
res.Add(key, val)
|
||||
// Using AddRaw instead of Add to save key-value pair as byte buffer within Header.
|
||||
// This buffer is used latter on in message writer to construct message and avoid crash
|
||||
// when key length is more than 76 characters long.
|
||||
res.AddRaw([]byte(key + ": " + val + "\r\n"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -52,6 +52,27 @@ func TestBuildPlainMessage(t *testing.T) {
|
||||
expectTransferEncoding(is(`quoted-printable`))
|
||||
}
|
||||
|
||||
func TestBuildPlainMessageWithLongKey(t *testing.T) {
|
||||
m := gomock.NewController(t)
|
||||
defer m.Finish()
|
||||
|
||||
b := NewBuilder(1, 1, 1)
|
||||
defer b.Done()
|
||||
|
||||
kr := tests.MakeKeyRing(t)
|
||||
msg := newTestMessage(t, kr, "messageID", "addressID", "text/plain", "body", time.Now())
|
||||
msg.Header["ReallyVeryVeryVeryVeryVeryLongLongLongLongLongLongLongKeyThatWillHaveNotSoLongValue"] = []string{"value"}
|
||||
|
||||
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
|
||||
require.NoError(t, err)
|
||||
|
||||
section(t, res).
|
||||
expectContentType(is(`text/plain`)).
|
||||
expectBody(is(`body`)).
|
||||
expectTransferEncoding(is(`quoted-printable`)).
|
||||
expectHeader(`ReallyVeryVeryVeryVeryVeryLongLongLongLongLongLongLongKeyThatWillHaveNotSoLongValue`, is(`value`))
|
||||
}
|
||||
|
||||
func TestBuildHTMLMessage(t *testing.T) {
|
||||
m := gomock.NewController(t)
|
||||
defer m.Finish()
|
||||
@ -99,6 +120,126 @@ func TestBuildPlainEncryptedMessage(t *testing.T) {
|
||||
expectBody(contains(`Where do fruits go on vacation? Pear-is!`))
|
||||
}
|
||||
|
||||
func TestBuildPlainEncryptedMessageMissingHeader(t *testing.T) {
|
||||
m := gomock.NewController(t)
|
||||
defer m.Finish()
|
||||
|
||||
b := NewBuilder(1, 1, 1)
|
||||
defer b.Done()
|
||||
|
||||
body := readerToString(getFileReader("plaintext-missing-header.eml"))
|
||||
|
||||
kr := tests.MakeKeyRing(t)
|
||||
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Now())
|
||||
|
||||
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
|
||||
require.NoError(t, err)
|
||||
|
||||
section(t, res).
|
||||
expectContentType(is(`text/plain`)).
|
||||
expectBody(is("How do we know that the ocean is friendly? It waves!\r\n"))
|
||||
}
|
||||
|
||||
func TestBuildPlainEncryptedMessageInvalidHeader(t *testing.T) {
|
||||
m := gomock.NewController(t)
|
||||
defer m.Finish()
|
||||
|
||||
b := NewBuilder(1, 1, 1)
|
||||
defer b.Done()
|
||||
|
||||
body := readerToString(getFileReader("plaintext-invalid-header.eml"))
|
||||
|
||||
kr := tests.MakeKeyRing(t)
|
||||
msg := newTestMessage(t, kr, "messageID", "addressID", "multipart/mixed", body, time.Now())
|
||||
|
||||
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
|
||||
require.NoError(t, err)
|
||||
|
||||
section(t, res).
|
||||
expectContentType(is(`text/plain`)).
|
||||
expectBody(is("MalformedKey Value\r\n\r\nHow do we know that the ocean is friendly? It waves!\r\n"))
|
||||
}
|
||||
|
||||
func TestBuildPlainSignedEncryptedMessageMissingHeader(t *testing.T) {
|
||||
m := gomock.NewController(t)
|
||||
defer m.Finish()
|
||||
|
||||
b := NewBuilder(1, 1, 1)
|
||||
defer b.Done()
|
||||
|
||||
body := readerToString(getFileReader("plaintext-missing-header.eml"))
|
||||
|
||||
kr := tests.MakeKeyRing(t)
|
||||
sig := tests.MakeKeyRing(t)
|
||||
|
||||
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig)
|
||||
require.NoError(t, err)
|
||||
|
||||
arm, err := enc.GetArmored()
|
||||
require.NoError(t, err)
|
||||
|
||||
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
|
||||
require.NoError(t, err)
|
||||
|
||||
section(t, res).
|
||||
expectContentType(is(`multipart/signed`)).
|
||||
expectContentTypeParam(`micalg`, is(`SHA-256`)). // NOTE: Maybe this is bad... should probably be pgp-sha256
|
||||
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)).
|
||||
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
|
||||
|
||||
section(t, res, 1).
|
||||
expectContentType(is(`text/plain`)).
|
||||
expectBody(is("How do we know that the ocean is friendly? It waves!\r\n"))
|
||||
|
||||
section(t, res, 2).
|
||||
expectContentType(is(`application/pgp-signature`)).
|
||||
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
|
||||
expectContentDisposition(is(`attachment`)).
|
||||
expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
|
||||
}
|
||||
|
||||
func TestBuildPlainSignedEncryptedMessageInvalidHeader(t *testing.T) {
|
||||
m := gomock.NewController(t)
|
||||
defer m.Finish()
|
||||
|
||||
b := NewBuilder(1, 1, 1)
|
||||
defer b.Done()
|
||||
|
||||
body := readerToString(getFileReader("plaintext-invalid-header.eml"))
|
||||
|
||||
kr := tests.MakeKeyRing(t)
|
||||
sig := tests.MakeKeyRing(t)
|
||||
|
||||
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), sig)
|
||||
require.NoError(t, err)
|
||||
|
||||
arm, err := enc.GetArmored()
|
||||
require.NoError(t, err)
|
||||
|
||||
msg := newRawTestMessage("messageID", "addressID", "multipart/mixed", arm, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
res, err := b.NewJob(context.Background(), newTestFetcher(m, kr, msg), msg.ID).GetResult()
|
||||
require.NoError(t, err)
|
||||
|
||||
section(t, res).
|
||||
expectContentType(is(`multipart/signed`)).
|
||||
expectContentTypeParam(`micalg`, is(`SHA-256`)). // NOTE: Maybe this is bad... should probably be pgp-sha256
|
||||
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)).
|
||||
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`))
|
||||
|
||||
section(t, res, 1).
|
||||
expectContentType(is(`text/plain`)).
|
||||
expectBody(is("MalformedKey Value\r\n\r\nHow do we know that the ocean is friendly? It waves!\r\n"))
|
||||
|
||||
section(t, res, 2).
|
||||
expectContentType(is(`application/pgp-signature`)).
|
||||
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).
|
||||
expectContentDisposition(is(`attachment`)).
|
||||
expectContentDispositionParam(`filename`, is(`OpenPGP_signature`))
|
||||
}
|
||||
|
||||
func TestBuildPlainEncryptedLatin2Message(t *testing.T) {
|
||||
m := gomock.NewController(t)
|
||||
defer m.Finish()
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"unicode"
|
||||
|
||||
"github.com/emersion/go-message/textproto"
|
||||
"github.com/pkg/errors"
|
||||
@ -37,8 +38,7 @@ func HeaderLines(header []byte) [][]byte {
|
||||
forEachLine(bufio.NewReader(bytes.NewReader(header)), func(line []byte) {
|
||||
l := bytes.SplitN(line, []byte(`: `), 2)
|
||||
isLineContinuation := quote%2 != 0 || // no quotes opened
|
||||
len(l) != 2 || // it doesn't have colon
|
||||
(len(l) == 2 && !bytes.Equal(bytes.TrimSpace(l[0]), l[0])) // has white space in front of header field
|
||||
!bytes.Equal(bytes.TrimLeftFunc(l[0], unicode.IsSpace), l[0]) // has whitespace indent at beginning
|
||||
switch {
|
||||
case len(bytes.TrimSpace(line)) == 0:
|
||||
lines = append(lines, line)
|
||||
@ -89,6 +89,12 @@ func readHeaderBody(b []byte) (*textproto.Header, []byte, error) {
|
||||
|
||||
var header textproto.Header
|
||||
|
||||
// We assume that everything before first occurrence of empty line is header.
|
||||
// If header is invalid for any reason or empty - put everything as body and let header be empty.
|
||||
if !isHeaderValid(lines) {
|
||||
return &header, b, nil
|
||||
}
|
||||
|
||||
// We add lines in reverse so that calling textproto.WriteHeader later writes with the correct order.
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
if len(bytes.TrimSpace(lines[i])) > 0 {
|
||||
@ -99,6 +105,20 @@ func readHeaderBody(b []byte) (*textproto.Header, []byte, error) {
|
||||
return &header, body, nil
|
||||
}
|
||||
|
||||
func isHeaderValid(headerLines [][]byte) bool {
|
||||
if len(headerLines) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, line := range headerLines {
|
||||
if (bytes.IndexByte(line, ':') == -1) && (len(bytes.TrimSpace(line)) > 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func splitHeaderBody(b []byte) ([]byte, []byte, error) {
|
||||
br := bufio.NewReader(bytes.NewReader(b))
|
||||
|
||||
|
||||
@ -79,3 +79,34 @@ Content-ID: <>
|
||||
[]byte("Content-ID: <>\n"),
|
||||
}, HeaderLines([]byte(header)))
|
||||
}
|
||||
|
||||
func TestReadHeaderBody(t *testing.T) {
|
||||
const data = "key: value\r\n\r\nbody\n"
|
||||
|
||||
header, body, err := readHeaderBody([]byte(data))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, header.Len())
|
||||
assert.Equal(t, "value", header.Get("key"))
|
||||
assert.Equal(t, []byte("body\n"), body)
|
||||
}
|
||||
|
||||
func TestReadHeaderBodyWithoutHeader(t *testing.T) {
|
||||
const data = "body\n"
|
||||
|
||||
header, body, err := readHeaderBody([]byte(data))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, header.Len())
|
||||
assert.Equal(t, []byte(data), body)
|
||||
}
|
||||
|
||||
func TestReadHeaderBodyInvalidHeader(t *testing.T) {
|
||||
const data = "value\r\n\r\nbody\n"
|
||||
|
||||
header, body, err := readHeaderBody([]byte(data))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, header.Len())
|
||||
assert.Equal(t, []byte(data), body)
|
||||
}
|
||||
|
||||
@ -528,6 +528,9 @@ func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
|
||||
if att.Name == "" {
|
||||
att.Name = mimeTypeParams["name"]
|
||||
}
|
||||
if att.Name == "" && mimeType == rfc822Message {
|
||||
att.Name = "message.eml"
|
||||
}
|
||||
if att.Name == "" {
|
||||
att.Name = "attachment.bin"
|
||||
}
|
||||
|
||||
@ -222,6 +222,22 @@ func TestParseTextPlainWithOctetAttachmentGoodFilename(t *testing.T) {
|
||||
assert.Equal(t, "😁😂.txt", m.Attachments[0].Name)
|
||||
}
|
||||
|
||||
func TestParseTextPlainWithRFC822Attachment(t *testing.T) {
|
||||
f := getFileReader("text_plain_rfc822_attachment.eml")
|
||||
|
||||
m, _, plainBody, attReaders, err := Parse(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
|
||||
|
||||
assert.Equal(t, "body", m.Body)
|
||||
assert.Equal(t, "body", plainBody)
|
||||
|
||||
assert.Len(t, attReaders, 1)
|
||||
assert.Equal(t, "message.eml", m.Attachments[0].Name)
|
||||
}
|
||||
|
||||
func TestParseTextPlainWithOctetAttachmentBadFilename(t *testing.T) {
|
||||
f := getFileReader("text_plain_octet_attachment_bad_2231_filename.eml")
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
|
||||
mediaType, params, _ := pmmime.ParseMediaType(info.Header.Get("Content-Type"))
|
||||
|
||||
// If multipart, call getAllParts, else read to count lines.
|
||||
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == "message/rfc822") && params["boundary"] != "" {
|
||||
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == rfc822Message) && params["boundary"] != "" {
|
||||
nextPath := getChildPath(currentPath)
|
||||
|
||||
var br *boundaryReader
|
||||
|
||||
3
pkg/message/testdata/plaintext-invalid-header.eml
vendored
Normal file
3
pkg/message/testdata/plaintext-invalid-header.eml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
MalformedKey Value
|
||||
|
||||
How do we know that the ocean is friendly? It waves!
|
||||
1
pkg/message/testdata/plaintext-missing-header.eml
vendored
Normal file
1
pkg/message/testdata/plaintext-missing-header.eml
vendored
Normal file
@ -0,0 +1 @@
|
||||
How do we know that the ocean is friendly? It waves!
|
||||
16
pkg/message/testdata/text_plain_rfc822_attachment.eml
vendored
Normal file
16
pkg/message/testdata/text_plain_rfc822_attachment.eml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
From: Sender <sender@pm.me>
|
||||
To: Receiver <receiver@pm.me>
|
||||
Content-Type: multipart/mixed; boundary=longrandomstring
|
||||
|
||||
--longrandomstring
|
||||
|
||||
body
|
||||
--longrandomstring
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: attachment
|
||||
|
||||
From: Sender <sender@pm.me>
|
||||
To: Receiver <receiver@pm.me>
|
||||
|
||||
inner body
|
||||
--longrandomstring--
|
||||
@ -143,6 +143,51 @@ func Test401RevokedAuth(t *testing.T) {
|
||||
r.EqualError(t, err, ErrUnauthorized.Error())
|
||||
}
|
||||
|
||||
func Test401RevokedAuthTokenUpdate(t *testing.T) {
|
||||
var oldAuth = &AuthRefresh{
|
||||
UID: "UID",
|
||||
AccessToken: "oldAcc",
|
||||
RefreshToken: "oldRef",
|
||||
ExpiresIn: 3600,
|
||||
}
|
||||
|
||||
var newAuth = &AuthRefresh{
|
||||
UID: "UID",
|
||||
AccessToken: "newAcc",
|
||||
RefreshToken: "newRef",
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(newAuth); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/addresses", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") == ("Bearer " + oldAuth.AccessToken) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Authorization") == ("Bearer " + newAuth.AccessToken) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
|
||||
c := New(Config{HostURL: ts.URL}).
|
||||
NewClient(oldAuth.UID, oldAuth.AccessToken, oldAuth.RefreshToken, time.Now().Add(time.Hour))
|
||||
|
||||
// The request will fail with 401, triggering a refresh. After the refresh it should succeed.
|
||||
_, err := c.GetAddresses(context.Background())
|
||||
r.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuth2FA(t *testing.T) {
|
||||
twoFACode := "code"
|
||||
|
||||
|
||||
@ -84,6 +84,8 @@ func (c *client) r(ctx context.Context) (*resty.Request, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// do executes fn and may repeate it in case "401 Unauthorized" error is returned.
|
||||
// Note: fn may be called more than once.
|
||||
func (c *client) do(ctx context.Context, fn func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
|
||||
r, err := c.r(ctx)
|
||||
if err != nil {
|
||||
@ -102,6 +104,12 @@ func (c *client) do(ctx context.Context, fn func(*resty.Request) (*resty.Respons
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to reconstruct request since access token is changed with authRefresh.
|
||||
r, err := c.r(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wrapNoConnection(fn(r))
|
||||
}
|
||||
|
||||
|
||||
@ -64,29 +64,6 @@ var errVerificationFailed = errors.New("signature verification failed")
|
||||
|
||||
// ================= Public utility functions ======================
|
||||
|
||||
func (c *client) EncryptAndSignCards(cards []Card) ([]Card, error) {
|
||||
var err error
|
||||
for i := range cards {
|
||||
card := &cards[i]
|
||||
if isEncryptedCardType(card.Type) {
|
||||
if isSignedCardType(card.Type) {
|
||||
if card.Signature, err = c.sign(card.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if card.Data, err = c.encrypt(card.Data, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if isSignedCardType(card.Type) {
|
||||
if card.Signature, err = c.sign(card.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
func (c *client) DecryptAndVerifyCards(cards []Card) ([]Card, error) {
|
||||
for i := range cards {
|
||||
card := &cards[i]
|
||||
|
||||
@ -209,20 +209,6 @@ var testCardsCleartext = []Card{
|
||||
},
|
||||
}
|
||||
|
||||
func TestClient_Encrypt(t *testing.T) {
|
||||
c := newClient(newManager(Config{}), "")
|
||||
c.userKeyRing = testPrivateKeyRing
|
||||
|
||||
cardEncrypted, err := c.EncryptAndSignCards(testCardsCleartext)
|
||||
r.Nil(t, err)
|
||||
|
||||
// Result is always different, so the best way is to test it by decrypting again.
|
||||
// Another test for decrypting will help us to be sure it's working.
|
||||
cardCleartext, err := c.DecryptAndVerifyCards(cardEncrypted)
|
||||
r.Nil(t, err)
|
||||
r.Equal(t, testCardsCleartext[0].Data, cardCleartext[0].Data)
|
||||
}
|
||||
|
||||
func TestClient_Decrypt(t *testing.T) {
|
||||
c := newClient(newManager(Config{}), "")
|
||||
c.userKeyRing = testPrivateKeyRing
|
||||
|
||||
@ -36,8 +36,8 @@ type PMKey struct {
|
||||
Fingerprint string
|
||||
PrivateKey *crypto.Key
|
||||
Primary int
|
||||
Token *string `json:",omitempty"`
|
||||
Signature *string `json:",omitempty"`
|
||||
Token string
|
||||
Signature string
|
||||
}
|
||||
|
||||
type clearable []byte
|
||||
@ -84,12 +84,12 @@ func (key PMKey) getPassphraseFromToken(kr *crypto.KeyRing) (passphrase []byte,
|
||||
return nil, errors.New("no user key was provided")
|
||||
}
|
||||
|
||||
msg, err := crypto.NewPGPMessageFromArmored(*key.Token)
|
||||
msg, err := crypto.NewPGPMessageFromArmored(key.Token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sig, err := crypto.NewPGPSignatureFromArmored(*key.Signature)
|
||||
sig, err := crypto.NewPGPSignatureFromArmored(key.Signature)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -137,7 +137,7 @@ func (keys *PMKeys) UnlockAll(passphrase []byte, userKey *crypto.KeyRing) (kr *c
|
||||
for _, key := range *keys {
|
||||
var secret []byte
|
||||
|
||||
if key.Token == nil || key.Signature == nil {
|
||||
if key.Token == "" || key.Signature == "" {
|
||||
secret = passphrase
|
||||
} else if secret, err = key.getPassphraseFromToken(userKey); err != nil {
|
||||
return
|
||||
@ -166,10 +166,6 @@ func (keys *PMKeys) UnlockAll(passphrase []byte, userKey *crypto.KeyRing) (kr *c
|
||||
// ErrNoKeyringAvailable represents an error caused by a keyring being nil or having no entities.
|
||||
var ErrNoKeyringAvailable = errors.New("no keyring available")
|
||||
|
||||
func (c *client) encrypt(plain string, signer *crypto.KeyRing) (armored string, err error) {
|
||||
return encrypt(c.userKeyRing, plain, signer)
|
||||
}
|
||||
|
||||
func encrypt(encrypter *crypto.KeyRing, plain string, signer *crypto.KeyRing) (armored string, err error) {
|
||||
if encrypter == nil {
|
||||
return "", ErrNoKeyringAvailable
|
||||
@ -209,18 +205,6 @@ func decrypt(decrypter *crypto.KeyRing, armored string) (plainBody []byte, err e
|
||||
return plainMessage.GetBinary(), nil
|
||||
}
|
||||
|
||||
func (c *client) sign(plain string) (armoredSignature string, err error) {
|
||||
if c.userKeyRing == nil {
|
||||
return "", ErrNoKeyringAvailable
|
||||
}
|
||||
plainMessage := crypto.NewPlainMessageFromString(plain)
|
||||
pgpSignature, err := c.userKeyRing.SignDetached(plainMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return pgpSignature.GetArmored()
|
||||
}
|
||||
|
||||
func (c *client) verify(plain, amroredSignature string) (err error) {
|
||||
plainMessage := crypto.NewPlainMessageFromString(plain)
|
||||
pgpSignature, err := crypto.NewPGPSignatureFromArmored(amroredSignature)
|
||||
|
||||
@ -1,9 +1,34 @@
|
||||
## v1.8.9
|
||||
- 2021-09-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issues with incorrect handling of 401 server error leading to random Bridge logouts
|
||||
- Changed encoding of message/rfc822 - to better handle sending of the .msg files
|
||||
- Fixed crash within RFC822 builder for invalid or empty headers
|
||||
- Fixed crash within RFC822 builder for header with key length > 76 chars
|
||||
|
||||
|
||||
## v1.8.7
|
||||
- 2021-06-22
|
||||
|
||||
### New
|
||||
|
||||
- Updated crypto-libraries to gopenpgp/v2 v2.1.10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed IMAP/SMTP restart in Bridge to mitigate connection issues
|
||||
- Fixed unknown charset error for 'combined' messages
|
||||
- Implemented a long-term fix for 'invalid or missing message signature' error
|
||||
|
||||
|
||||
## v1.8.5
|
||||
- 2021-06-11
|
||||
|
||||
### New
|
||||
|
||||
- Implemented golang Secure Remote Password Protocol
|
||||
- Updated golang Secure Remote Password Protocol
|
||||
- Updated crypto-libraries to gopenpgp/v2 v2.1.9
|
||||
- Implemented new message parser (for imports from external accounts)
|
||||
|
||||
|
||||
@ -1,3 +1,24 @@
|
||||
## v1.8.7
|
||||
- 2021-06-24
|
||||
|
||||
### New
|
||||
|
||||
- Updated golang Secure Remote Password Protocol
|
||||
- Updated crypto-libraries to gopenpgp/v2 v2.1.10
|
||||
- Implemented new message parser (for imports from external accounts)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed IMAP/SMTP restart in Bridge to mitigate connection issues
|
||||
- Fixed unknown charset error for 'combined' messages
|
||||
- Implemented a long-term fix for 'invalid or missing message signature' error
|
||||
- Bridge not to strip PGP signatures of incoming clear text messages
|
||||
- Import of messages with malformed MIME header
|
||||
- Improved parsing of message headers
|
||||
- Fetching bodies of non-multipart messages
|
||||
- Sync and performance improvements
|
||||
|
||||
|
||||
## v1.8.3
|
||||
- 2021-05-27
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.PHONY: check-go check-godog install-godog test test-bridge test-ie test-live test-live-bridge test-live-ie test-stage test-debug test-live-debug bench
|
||||
|
||||
export GO111MODULE=on
|
||||
export BRIDGE_VERSION:=1.8.7+integrationtests
|
||||
export BRIDGE_VERSION:=1.8.9+integrationtests
|
||||
export VERBOSITY?=fatal
|
||||
export TEST_DATA=testdata
|
||||
export TEST_APP?=bridge
|
||||
|
||||
@ -75,5 +75,10 @@ func (api *FakePMAPI) CreateAttachment(_ context.Context, attachment *pmapi.Atta
|
||||
return nil, err
|
||||
}
|
||||
attachment.KeyPackets = base64.StdEncoding.EncodeToString(bytes)
|
||||
msg := api.getMessage(attachment.MessageID)
|
||||
if msg == nil {
|
||||
return nil, fmt.Errorf("no such message ID %q", attachment.MessageID)
|
||||
}
|
||||
msg.Attachments = append(msg.Attachments, attachment)
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
@ -34,10 +34,8 @@ func (api *FakePMAPI) GetMessage(_ context.Context, apiID string) (*pmapi.Messag
|
||||
if err := api.checkAndRecordCall(GET, "/mail/v4/messages/"+apiID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, message := range api.messages {
|
||||
if message.ID == apiID {
|
||||
return message, nil
|
||||
}
|
||||
if msg := api.getMessage(apiID); msg != nil {
|
||||
return msg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("message %s not found", apiID)
|
||||
}
|
||||
@ -175,8 +173,8 @@ func (api *FakePMAPI) SendMessage(ctx context.Context, messageID string, sendMes
|
||||
if err := api.checkAndRecordCall(POST, "/mail/v4/messages/"+messageID, sendMessageRequest); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
message, err := api.GetMessage(ctx, messageID)
|
||||
if err != nil {
|
||||
message := api.getMessage(messageID)
|
||||
if message == nil {
|
||||
return nil, nil, errors.Wrap(err, "draft does not exist")
|
||||
}
|
||||
message.Time = time.Now().Unix()
|
||||
@ -276,6 +274,15 @@ func (api *FakePMAPI) findMessage(newMsg *pmapi.Message) *pmapi.Message {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) getMessage(msgID string) *pmapi.Message {
|
||||
for _, msg := range api.messages {
|
||||
if msg.ID == msgID {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) addMessage(message *pmapi.Message) {
|
||||
if api.findMessage(message) != nil {
|
||||
return
|
||||
|
||||
@ -120,3 +120,91 @@ Feature: SMTP sending of HTML messages with attachments
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Alternative plain and HTML message with rfc822 attachment
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <[userAddress]>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Alternative plain and HTML with rfc822 attachment
|
||||
Content-Type: multipart/mixed; boundary=main-parts
|
||||
|
||||
This is a multipart message in MIME format
|
||||
|
||||
--main-parts
|
||||
Content-Type: multipart/alternative; boundary=alternatives
|
||||
|
||||
--alternatives
|
||||
Content-Type: text/plain
|
||||
|
||||
There is an attachment
|
||||
|
||||
|
||||
--alternatives
|
||||
Content-Type: text/html
|
||||
|
||||
<html><body>There <b>is</b> an attachment<body></html>
|
||||
|
||||
|
||||
--alternatives--
|
||||
|
||||
--main-parts
|
||||
Content-Type: message/rfc822
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: attachment
|
||||
|
||||
Received: from mx1.opensuse.org (mx1.infra.opensuse.org [192.168.47.95]) by
|
||||
mailman3.infra.opensuse.org (Postfix) with ESMTP id 38BE2AC3 for
|
||||
<factory@lists.opensuse.org>; Sun, 11 Jul 2021 19:50:34 +0000 (UTC)
|
||||
From: "Bob " <Bob@something.net>
|
||||
Sender: "Bob" <Bob@gmail.com>
|
||||
To: "opensuse-factory" <opensuse-factory@opensuse.org>
|
||||
Cc: "Bob" <Bob@something.net>
|
||||
References: <y6ZUV5yEyOVQHETZRmi1GFe-Xumzct7QcLpGoSsi1MefGaoovfrUqdkmQ5gM6uySZ7JPIJhDkPJFDqHS1fb_mQ==@protonmail.internalid>
|
||||
Subject: VirtualBox problems with kernel 5.13
|
||||
Date: Sun, 11 Jul 2021 21:50:25 +0200
|
||||
Message-ID: <71672e5f-24a2-c79f-03cc-4c923eb1790b@lwfinger.net>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
X-Mailer: Microsoft Outlook 16.0
|
||||
List-Unsubscribe: <mailto:factory-leave@lists.opensuse.org>
|
||||
Content-Language: en-us
|
||||
List-Help: <mailto:factory-request@lists.opensuse.org?subject=help>
|
||||
List-Subscribe: <mailto:factory-join@lists.opensuse.org>
|
||||
Thread-Index: AQFWvbNSAqFOch49YPlLU4eJWPObaQK2iKDq
|
||||
|
||||
I am writing this message as openSUSE's maintainer of VirtualBox.
|
||||
|
||||
Nearly every update of the Linux kernel to a new 5.X version breaks =
|
||||
VirtualBox.
|
||||
|
||||
Bob
|
||||
|
||||
--main-parts--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | Alternative plain and HTML with rfc822 attachment |
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Alternative plain and HTML with rfc822 attachment",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/html"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user