Add a streaming api to KeyRing and SessionKey (#131)
* barebone streaming functionality * encryption needs to return a writecloser * added eof check * workaround for reader problem with copies * separate mobile wrappers from main api * add a clone in the read result to avoid memory corruption * refactor to reuse code, and fix verification * have to give the verify key at the start of the decryption * enfore readAll before signature verification * streaming api for SessionKey * add split message stream apis * name interface params * fix streaming api so it's supported by go-mobile * hide internal writeCloser * fix nil access * added detached sigs methods * started unit testing * unit testing and fixed a bug where key and data packets where inverted * remove unecessary error wrapping * figured out closing order and error handling * add GC calls to mobile writer and reader * remove debugging values and arrays * writer with builtin sha256 * unit testing the mobile helpers * comments and linting * Typo in error Co-authored-by: wussler <aron@wussler.it> * Add GetKeyPacket doc Co-authored-by: wussler <aron@wussler.it> * Add rfc reference in comments Co-authored-by: wussler <aron@wussler.it> * small improvements * add compatibility tests with normal methods * remove unecessary copies in the tests * update go-crypto to the merged changes commit * update comments of core internal functions * remove unused nolint comment * group message metadata in a struct * fix comments * change default values for metadata * change the mobile reader wrapper to fit the behavior of java * remove gc calls in the wrappers to avoid performance penalties * bring back the former Go2MobileReader to be used for ios * Update crypto/keyring_streaming.go Co-authored-by: wussler <aron@wussler.it> * return an error when verifying an embedded sig with no keyring * Update crypto/sessionkey_streaming.go Co-authored-by: wussler <aron@wussler.it> * linter error * update changelog * update changelog Co-authored-by: wussler <aron@wussler.it>
This commit is contained in:
parent
7380f7391f
commit
c46ed8ed9e
11 changed files with 1718 additions and 97 deletions
182
helper/mobile_stream.go
Normal file
182
helper/mobile_stream.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Mobile2GoWriter is used to wrap a writer in the mobile app runtime,
|
||||
// to be usable in the golang runtime (via gomobile).
|
||||
type Mobile2GoWriter struct {
|
||||
writer crypto.Writer
|
||||
}
|
||||
|
||||
// NewMobile2GoWriter wraps a writer to be usable in the golang runtime (via gomobile).
|
||||
func NewMobile2GoWriter(writer crypto.Writer) *Mobile2GoWriter {
|
||||
return &Mobile2GoWriter{writer}
|
||||
}
|
||||
|
||||
// Write writes the data in the provided buffer in the wrapped writer.
|
||||
// It clones the provided data to prevent errors with garbage collectors.
|
||||
func (w *Mobile2GoWriter) Write(b []byte) (n int, err error) {
|
||||
bufferCopy := clone(b)
|
||||
return w.writer.Write(bufferCopy)
|
||||
}
|
||||
|
||||
// Mobile2GoWriterWithSHA256 is used to wrap a writer in the mobile app runtime,
|
||||
// to be usable in the golang runtime (via gomobile).
|
||||
// It also computes the SHA256 hash of the data being written on the fly.
|
||||
type Mobile2GoWriterWithSHA256 struct {
|
||||
writer crypto.Writer
|
||||
sha256 hash.Hash
|
||||
}
|
||||
|
||||
// NewMobile2GoWriterWithSHA256 wraps a writer to be usable in the golang runtime (via gomobile).
|
||||
// The wrapper also computes the SHA256 hash of the data being written on the fly.
|
||||
func NewMobile2GoWriterWithSHA256(writer crypto.Writer) *Mobile2GoWriterWithSHA256 {
|
||||
return &Mobile2GoWriterWithSHA256{writer, sha256.New()}
|
||||
}
|
||||
|
||||
// Write writes the data in the provided buffer in the wrapped writer.
|
||||
// It clones the provided data to prevent errors with garbage collectors.
|
||||
// It also computes the SHA256 hash of the data being written on the fly.
|
||||
func (w *Mobile2GoWriterWithSHA256) Write(b []byte) (n int, err error) {
|
||||
bufferCopy := clone(b)
|
||||
n, err = w.writer.Write(bufferCopy)
|
||||
if err == nil {
|
||||
hashedTotal := 0
|
||||
for hashedTotal < n {
|
||||
hashed, err := w.sha256.Write(bufferCopy[hashedTotal:n])
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "gopenpgp: couldn't hash encrypted data")
|
||||
}
|
||||
hashedTotal += hashed
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// GetSHA256 returns the SHA256 hash of the data that's been written so far.
|
||||
func (w *Mobile2GoWriterWithSHA256) GetSHA256() []byte {
|
||||
return w.sha256.Sum(nil)
|
||||
}
|
||||
|
||||
// MobileReader is the interface that readers in the mobile runtime must use and implement.
|
||||
// This is a workaround to some of the gomobile limitations.
|
||||
type MobileReader interface {
|
||||
Read(max int) (result *MobileReadResult, err error)
|
||||
}
|
||||
|
||||
// MobileReadResult is what needs to be returned by MobileReader.Read.
|
||||
// The read data is passed as a return value rather than passed as an argument to the reader.
|
||||
// This avoids problems introduced by gomobile that prevent the use of native golang readers.
|
||||
type MobileReadResult struct {
|
||||
N int // N, The number of bytes read
|
||||
IsEOF bool // IsEOF, If true, then the reader has reached the end of the data to read.
|
||||
Data []byte // Data, the data that has been read
|
||||
}
|
||||
|
||||
// NewMobileReadResult initialize a MobileReadResult with the correct values.
|
||||
// It clones the data to avoid the garbage collector freeing the data too early.
|
||||
func NewMobileReadResult(n int, eof bool, data []byte) *MobileReadResult {
|
||||
return &MobileReadResult{N: n, IsEOF: eof, Data: clone(data)}
|
||||
}
|
||||
|
||||
func clone(src []byte) (dst []byte) {
|
||||
dst = make([]byte, len(src))
|
||||
copy(dst, src)
|
||||
return
|
||||
}
|
||||
|
||||
// Mobile2GoReader is used to wrap a MobileReader in the mobile app runtime,
|
||||
// to be usable in the golang runtime (via gomobile) as a native Reader.
|
||||
type Mobile2GoReader struct {
|
||||
reader MobileReader
|
||||
}
|
||||
|
||||
// NewMobile2GoReader wraps a MobileReader to be usable in the golang runtime (via gomobile).
|
||||
func NewMobile2GoReader(reader MobileReader) *Mobile2GoReader {
|
||||
return &Mobile2GoReader{reader}
|
||||
}
|
||||
|
||||
// Read reads data from the wrapped MobileReader and copies the read data in the provided buffer.
|
||||
// It also handles the conversion of EOF to an error.
|
||||
func (r *Mobile2GoReader) Read(b []byte) (n int, err error) {
|
||||
result, err := r.reader.Read(len(b))
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "gopenpgp: couldn't read from mobile reader")
|
||||
}
|
||||
n = result.N
|
||||
if n > 0 {
|
||||
copy(b, result.Data[:n])
|
||||
}
|
||||
if result.IsEOF {
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Go2AndroidReader is used to wrap a native golang Reader in the golang runtime,
|
||||
// to be usable in the android app runtime (via gomobile).
|
||||
type Go2AndroidReader struct {
|
||||
isEOF bool
|
||||
reader crypto.Reader
|
||||
}
|
||||
|
||||
// NewGo2AndroidReader wraps a native golang Reader to be usable in the mobile app runtime (via gomobile).
|
||||
// It doesn't follow the standard golang Reader behavior, and returns n = -1 on EOF.
|
||||
func NewGo2AndroidReader(reader crypto.Reader) *Go2AndroidReader {
|
||||
return &Go2AndroidReader{isEOF: false, reader: reader}
|
||||
}
|
||||
|
||||
// Read reads bytes into the provided buffer and returns the number of bytes read
|
||||
// It doesn't follow the standard golang Reader behavior, and returns n = -1 on EOF.
|
||||
func (r *Go2AndroidReader) Read(b []byte) (n int, err error) {
|
||||
if r.isEOF {
|
||||
return -1, nil
|
||||
}
|
||||
n, err = r.reader.Read(b)
|
||||
if errors.Is(err, io.EOF) {
|
||||
if n == 0 {
|
||||
return -1, nil
|
||||
} else {
|
||||
r.isEOF = true
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Go2IOSReader is used to wrap a native golang Reader in the golang runtime,
|
||||
// to be usable in the iOS app runtime (via gomobile) as a MobileReader.
|
||||
type Go2IOSReader struct {
|
||||
reader crypto.Reader
|
||||
}
|
||||
|
||||
// NewGo2IOSReader wraps a native golang Reader to be usable in the ios app runtime (via gomobile).
|
||||
func NewGo2IOSReader(reader crypto.Reader) *Go2IOSReader {
|
||||
return &Go2IOSReader{reader}
|
||||
}
|
||||
|
||||
// Read reads at most <max> bytes from the wrapped Reader and returns the read data as a MobileReadResult.
|
||||
func (r *Go2IOSReader) Read(max int) (result *MobileReadResult, err error) {
|
||||
b := make([]byte, max)
|
||||
n, err := r.reader.Read(b)
|
||||
result = &MobileReadResult{}
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
result.IsEOF = true
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result.N = n
|
||||
if n > 0 {
|
||||
result.Data = b[:n]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
182
helper/mobile_stream_test.go
Normal file
182
helper/mobile_stream_test.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func cloneTestData() (a, b []byte) {
|
||||
a = []byte("Hello World!")
|
||||
b = clone(a)
|
||||
return a, b
|
||||
}
|
||||
func Test_clone(t *testing.T) {
|
||||
if a, b := cloneTestData(); !bytes.Equal(a, b) {
|
||||
t.Fatalf("expected %x, got %x", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobile2GoWriter(t *testing.T) {
|
||||
testData := []byte("Hello World!")
|
||||
outBuf := &bytes.Buffer{}
|
||||
reader := bytes.NewReader(testData)
|
||||
writer := NewMobile2GoWriter(outBuf)
|
||||
bufSize := 2
|
||||
writeBuf := make([]byte, bufSize)
|
||||
reachedEnd := false
|
||||
for !reachedEnd {
|
||||
n, err := reader.Read(writeBuf)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
reachedEnd = true
|
||||
} else {
|
||||
t.Fatal("Expected no error while reading, got:", err)
|
||||
}
|
||||
}
|
||||
writtenTotal := 0
|
||||
for writtenTotal < n {
|
||||
written, err := writer.Write(writeBuf[writtenTotal:n])
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while writing, got:", err)
|
||||
}
|
||||
writtenTotal += written
|
||||
}
|
||||
}
|
||||
if writtenData := outBuf.Bytes(); !bytes.Equal(testData, writtenData) {
|
||||
t.Fatalf("expected %x, got %x", testData, writtenData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobile2GoWriterWithSHA256(t *testing.T) {
|
||||
testData := []byte("Hello World!")
|
||||
testHash := sha256.Sum256(testData)
|
||||
outBuf := &bytes.Buffer{}
|
||||
reader := bytes.NewReader(testData)
|
||||
writer := NewMobile2GoWriterWithSHA256(outBuf)
|
||||
bufSize := 2
|
||||
writeBuf := make([]byte, bufSize)
|
||||
reachedEnd := false
|
||||
for !reachedEnd {
|
||||
n, err := reader.Read(writeBuf)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
reachedEnd = true
|
||||
} else {
|
||||
t.Fatal("Expected no error while reading, got:", err)
|
||||
}
|
||||
}
|
||||
writtenTotal := 0
|
||||
for writtenTotal < n {
|
||||
written, err := writer.Write(writeBuf[writtenTotal:n])
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while writing, got:", err)
|
||||
}
|
||||
writtenTotal += written
|
||||
}
|
||||
}
|
||||
if writtenData := outBuf.Bytes(); !bytes.Equal(testData, writtenData) {
|
||||
t.Fatalf("expected data to be %x, got %x", testData, writtenData)
|
||||
}
|
||||
|
||||
if writtenHash := writer.GetSHA256(); !bytes.Equal(testHash[:], writtenHash) {
|
||||
t.Fatalf("expected has to be %x, got %x", testHash, writtenHash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGo2AndroidReader(t *testing.T) {
|
||||
testData := []byte("Hello World!")
|
||||
reader := NewGo2AndroidReader(bytes.NewReader(testData))
|
||||
var readData []byte
|
||||
bufSize := 2
|
||||
buffer := make([]byte, bufSize)
|
||||
reachedEnd := false
|
||||
for !reachedEnd {
|
||||
n, err := reader.Read(buffer)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while reading, got:", err)
|
||||
}
|
||||
reachedEnd = n < 0
|
||||
if n > 0 {
|
||||
readData = append(readData, buffer[:n]...)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(testData, readData) {
|
||||
t.Fatalf("expected data to be %x, got %x", testData, readData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGo2IOSReader(t *testing.T) {
|
||||
testData := []byte("Hello World!")
|
||||
reader := NewGo2IOSReader(bytes.NewReader(testData))
|
||||
var readData []byte
|
||||
bufSize := 2
|
||||
reachedEnd := false
|
||||
for !reachedEnd {
|
||||
res, err := reader.Read(bufSize)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while reading, got:", err)
|
||||
}
|
||||
n := res.N
|
||||
reachedEnd = res.IsEOF
|
||||
if n > 0 {
|
||||
readData = append(readData, res.Data[:n]...)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(testData, readData) {
|
||||
t.Fatalf("expected data to be %x, got %x", testData, readData)
|
||||
}
|
||||
}
|
||||
|
||||
type testMobileReader struct {
|
||||
reader io.Reader
|
||||
returnError bool
|
||||
}
|
||||
|
||||
func (r *testMobileReader) Read(max int) (*MobileReadResult, error) {
|
||||
if r.returnError {
|
||||
return nil, errors.New("gopenpgp: test - forced error while reading")
|
||||
}
|
||||
buf := make([]byte, max)
|
||||
n, err := r.reader.Read(buf)
|
||||
eof := false
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
eof = true
|
||||
} else {
|
||||
return nil, errors.New("gopenpgp: test - error while reading")
|
||||
}
|
||||
}
|
||||
return NewMobileReadResult(n, eof, buf[:n]), nil
|
||||
}
|
||||
|
||||
func TestMobile2GoReader(t *testing.T) {
|
||||
testData := []byte("Hello World!")
|
||||
reader := NewMobile2GoReader(&testMobileReader{bytes.NewReader(testData), false})
|
||||
var readData []byte
|
||||
bufSize := 2
|
||||
readBuf := make([]byte, bufSize)
|
||||
reachedEnd := false
|
||||
for !reachedEnd {
|
||||
n, err := reader.Read(readBuf)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
reachedEnd = true
|
||||
} else {
|
||||
t.Fatal("Expected no error while reading, got:", err)
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
readData = append(readData, readBuf[:n]...)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(testData, readData) {
|
||||
t.Fatalf("expected data to be %x, got %x", testData, readData)
|
||||
}
|
||||
readerErr := NewMobile2GoReader(&testMobileReader{bytes.NewReader(testData), true})
|
||||
if _, err := readerErr.Read(readBuf); err == nil {
|
||||
t.Fatal("expected an error while reading, got nil")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue