summary refs log tree commit diff
path: root/vendor/maunium.net/go/mautrix/crypto/attachment
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maunium.net/go/mautrix/crypto/attachment')
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go300
1 files changed, 300 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go b/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go
new file mode 100644
index 0000000..cfa1c3e
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go
@@ -0,0 +1,300 @@
+// Copyright (c) 2022 Tulir Asokan
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package attachment
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/sha256"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"hash"
+	"io"
+
+	"maunium.net/go/mautrix/crypto/utils"
+)
+
+var (
+	HashMismatch         = errors.New("mismatching SHA-256 digest")
+	UnsupportedVersion   = errors.New("unsupported Matrix file encryption version")
+	UnsupportedAlgorithm = errors.New("unsupported JWK encryption algorithm")
+	InvalidKey           = errors.New("failed to decode key")
+	InvalidInitVector    = errors.New("failed to decode initialization vector")
+	InvalidHash          = errors.New("failed to decode SHA-256 hash")
+	ReaderClosed         = errors.New("encrypting reader was already closed")
+)
+
+var (
+	keyBase64Length  = base64.RawURLEncoding.EncodedLen(utils.AESCTRKeyLength)
+	ivBase64Length   = base64.RawStdEncoding.EncodedLen(utils.AESCTRIVLength)
+	hashBase64Length = base64.RawStdEncoding.EncodedLen(utils.SHAHashLength)
+)
+
+type JSONWebKey struct {
+	Key         string   `json:"k"`
+	Algorithm   string   `json:"alg"`
+	Extractable bool     `json:"ext"`
+	KeyType     string   `json:"kty"`
+	KeyOps      []string `json:"key_ops"`
+}
+
+type EncryptedFileHashes struct {
+	SHA256 string `json:"sha256"`
+}
+
+type decodedKeys struct {
+	key [utils.AESCTRKeyLength]byte
+	iv  [utils.AESCTRIVLength]byte
+
+	sha256 [utils.SHAHashLength]byte
+}
+
+type EncryptedFile struct {
+	Key        JSONWebKey          `json:"key"`
+	InitVector string              `json:"iv"`
+	Hashes     EncryptedFileHashes `json:"hashes"`
+	Version    string              `json:"v"`
+
+	decoded *decodedKeys
+}
+
+func NewEncryptedFile() *EncryptedFile {
+	key, iv := utils.GenAttachmentA256CTR()
+	return &EncryptedFile{
+		Key: JSONWebKey{
+			Key:         base64.RawURLEncoding.EncodeToString(key[:]),
+			Algorithm:   "A256CTR",
+			Extractable: true,
+			KeyType:     "oct",
+			KeyOps:      []string{"encrypt", "decrypt"},
+		},
+		InitVector: base64.RawStdEncoding.EncodeToString(iv[:]),
+		Version:    "v2",
+
+		decoded: &decodedKeys{key: key, iv: iv},
+	}
+}
+
+func (ef *EncryptedFile) decodeKeys(includeHash bool) error {
+	if ef.decoded != nil {
+		return nil
+	} else if len(ef.Key.Key) != keyBase64Length {
+		return InvalidKey
+	} else if len(ef.InitVector) != ivBase64Length {
+		return InvalidInitVector
+	} else if includeHash && len(ef.Hashes.SHA256) != hashBase64Length {
+		return InvalidHash
+	}
+	ef.decoded = &decodedKeys{}
+	_, err := base64.RawURLEncoding.Decode(ef.decoded.key[:], []byte(ef.Key.Key))
+	if err != nil {
+		return InvalidKey
+	}
+	_, err = base64.RawStdEncoding.Decode(ef.decoded.iv[:], []byte(ef.InitVector))
+	if err != nil {
+		return InvalidInitVector
+	}
+	if includeHash {
+		_, err = base64.RawStdEncoding.Decode(ef.decoded.sha256[:], []byte(ef.Hashes.SHA256))
+		if err != nil {
+			return InvalidHash
+		}
+	}
+	return nil
+}
+
+// Encrypt encrypts the given data, updates the SHA256 hash in the EncryptedFile struct and returns the ciphertext.
+//
+// Deprecated: this makes a copy for the ciphertext, which means 2x memory usage. EncryptInPlace is recommended.
+func (ef *EncryptedFile) Encrypt(plaintext []byte) []byte {
+	ciphertext := make([]byte, len(plaintext))
+	copy(ciphertext, plaintext)
+	ef.EncryptInPlace(ciphertext)
+	return ciphertext
+}
+
+// EncryptInPlace encrypts the given data in-place (i.e. the provided data is overridden with the ciphertext)
+// and updates the SHA256 hash in the EncryptedFile struct.
+func (ef *EncryptedFile) EncryptInPlace(data []byte) {
+	ef.decodeKeys(false)
+	utils.XorA256CTR(data, ef.decoded.key, ef.decoded.iv)
+	checksum := sha256.Sum256(data)
+	ef.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(checksum[:])
+}
+
+type ReadWriterAt interface {
+	io.WriterAt
+	io.Reader
+}
+
+// EncryptFile encrypts the given file in-place and updates the SHA256 hash in the EncryptedFile struct.
+func (ef *EncryptedFile) EncryptFile(file ReadWriterAt) error {
+	err := ef.decodeKeys(false)
+	if err != nil {
+		return err
+	}
+	block, _ := aes.NewCipher(ef.decoded.key[:])
+	stream := cipher.NewCTR(block, ef.decoded.iv[:])
+	hasher := sha256.New()
+	buf := make([]byte, 32*1024)
+	var writePtr int64
+	var n int
+	for {
+		n, err = file.Read(buf)
+		if err != nil && !errors.Is(err, io.EOF) {
+			return err
+		}
+		if n == 0 {
+			break
+		}
+		stream.XORKeyStream(buf[:n], buf[:n])
+		_, err = file.WriteAt(buf[:n], writePtr)
+		if err != nil {
+			return err
+		}
+		writePtr += int64(n)
+		hasher.Write(buf[:n])
+	}
+	ef.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(hasher.Sum(nil))
+	return nil
+}
+
+type encryptingReader struct {
+	stream cipher.Stream
+	hash   hash.Hash
+	source io.Reader
+	file   *EncryptedFile
+	closed bool
+
+	isDecrypting bool
+}
+
+var _ io.ReadSeekCloser = (*encryptingReader)(nil)
+
+func (r *encryptingReader) Seek(offset int64, whence int) (int64, error) {
+	if r.closed {
+		return 0, ReaderClosed
+	}
+	if offset != 0 || whence != io.SeekStart {
+		return 0, fmt.Errorf("attachments.EncryptStream: only seeking to the beginning is supported")
+	}
+	seeker, ok := r.source.(io.ReadSeeker)
+	if !ok {
+		return 0, fmt.Errorf("attachments.EncryptStream: source reader (%T) is not an io.ReadSeeker", r.source)
+	}
+	n, err := seeker.Seek(offset, whence)
+	if err != nil {
+		return 0, err
+	}
+	block, _ := aes.NewCipher(r.file.decoded.key[:])
+	r.stream = cipher.NewCTR(block, r.file.decoded.iv[:])
+	r.hash.Reset()
+	return n, nil
+}
+
+func (r *encryptingReader) Read(dst []byte) (n int, err error) {
+	if r.closed {
+		return 0, ReaderClosed
+	} else if r.isDecrypting && r.file.decoded == nil {
+		if err = r.file.PrepareForDecryption(); err != nil {
+			return
+		}
+	}
+	n, err = r.source.Read(dst)
+	r.stream.XORKeyStream(dst[:n], dst[:n])
+	r.hash.Write(dst[:n])
+	return
+}
+
+func (r *encryptingReader) Close() (err error) {
+	closer, ok := r.source.(io.ReadCloser)
+	if ok {
+		err = closer.Close()
+	}
+	if r.isDecrypting {
+		var downloadedChecksum [utils.SHAHashLength]byte
+		r.hash.Sum(downloadedChecksum[:])
+		if downloadedChecksum != r.file.decoded.sha256 {
+			return HashMismatch
+		}
+	} else {
+		r.file.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(r.hash.Sum(nil))
+	}
+	r.closed = true
+	return
+}
+
+// EncryptStream wraps the given io.Reader in order to encrypt the data.
+//
+// The Close() method of the returned io.ReadCloser must be called for the SHA256 hash
+// in the EncryptedFile struct to be updated. The metadata is not valid before the hash
+// is filled.
+func (ef *EncryptedFile) EncryptStream(reader io.Reader) io.ReadSeekCloser {
+	ef.decodeKeys(false)
+	block, _ := aes.NewCipher(ef.decoded.key[:])
+	return &encryptingReader{
+		stream: cipher.NewCTR(block, ef.decoded.iv[:]),
+		hash:   sha256.New(),
+		source: reader,
+		file:   ef,
+	}
+}
+
+// Decrypt decrypts the given data and returns the plaintext.
+//
+// Deprecated: this makes a copy for the plaintext data, which means 2x memory usage. DecryptInPlace is recommended.
+func (ef *EncryptedFile) Decrypt(ciphertext []byte) ([]byte, error) {
+	plaintext := make([]byte, len(ciphertext))
+	copy(plaintext, ciphertext)
+	return plaintext, ef.DecryptInPlace(plaintext)
+}
+
+// PrepareForDecryption checks that the version and algorithm are supported and decodes the base64 keys
+//
+// DecryptStream will call this with the first Read() call if this hasn't been called manually.
+//
+// DecryptInPlace will always call this automatically, so calling this manually is not necessary when using that function.
+func (ef *EncryptedFile) PrepareForDecryption() error {
+	if ef.Version != "v2" {
+		return UnsupportedVersion
+	} else if ef.Key.Algorithm != "A256CTR" {
+		return UnsupportedAlgorithm
+	} else if err := ef.decodeKeys(true); err != nil {
+		return err
+	}
+	return nil
+}
+
+// DecryptInPlace decrypts the given data in-place (i.e. the provided data is overridden with the plaintext).
+func (ef *EncryptedFile) DecryptInPlace(data []byte) error {
+	if err := ef.PrepareForDecryption(); err != nil {
+		return err
+	} else if ef.decoded.sha256 != sha256.Sum256(data) {
+		return HashMismatch
+	} else {
+		utils.XorA256CTR(data, ef.decoded.key, ef.decoded.iv)
+		return nil
+	}
+}
+
+// DecryptStream wraps the given io.Reader in order to decrypt the data.
+//
+// The first Read call will check the algorithm and decode keys, so it might return an error before actually reading anything.
+// If you want to validate the file before opening the stream, call PrepareForDecryption manually and check for errors.
+//
+// The Close call will validate the hash and return an error if it doesn't match.
+// In this case, the written data should be considered compromised and should not be used further.
+func (ef *EncryptedFile) DecryptStream(reader io.Reader) io.ReadSeekCloser {
+	block, _ := aes.NewCipher(ef.decoded.key[:])
+	return &encryptingReader{
+		stream: cipher.NewCTR(block, ef.decoded.iv[:]),
+		hash:   sha256.New(),
+		source: reader,
+		file:   ef,
+	}
+}