diff options
Diffstat (limited to 'vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go')
-rw-r--r-- | vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go | 239 |
1 files changed, 239 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 00000000..e516fded --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go @@ -0,0 +1,239 @@ +// 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" + "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 encryptingReader struct { + stream cipher.Stream + hash hash.Hash + source io.Reader + file *EncryptedFile + closed bool + + isDecrypting bool +} + +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.ReadCloser { + 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.ReadCloser { + block, _ := aes.NewCipher(ef.decoded.key[:]) + return &encryptingReader{ + stream: cipher.NewCTR(block, ef.decoded.iv[:]), + hash: sha256.New(), + source: reader, + file: ef, + } +} |