summaryrefslogtreecommitdiffstats
path: root/vendor/maunium.net/go/mautrix/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maunium.net/go/mautrix/crypto')
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go239
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/utils/utils.go133
2 files changed, 372 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,
+ }
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/utils/utils.go b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go
new file mode 100644
index 00000000..e320bca1
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go
@@ -0,0 +1,133 @@
+// Copyright (c) 2020 Nikos Filippakis
+//
+// 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 utils
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/base64"
+ "math/rand"
+ "strings"
+
+ "golang.org/x/crypto/hkdf"
+ "golang.org/x/crypto/pbkdf2"
+
+ "maunium.net/go/mautrix/util/base58"
+)
+
+const (
+ // AESCTRKeyLength is the length of the AES256-CTR key used.
+ AESCTRKeyLength = 32
+ // AESCTRIVLength is the length of the AES256-CTR IV used.
+ AESCTRIVLength = 16
+ // HMACKeyLength is the length of the HMAC key used.
+ HMACKeyLength = 32
+ // SHAHashLength is the length of the SHA hash used.
+ SHAHashLength = 32
+)
+
+// XorA256CTR encrypts the input with the keystream generated by the AES256-CTR algorithm with the given arguments.
+func XorA256CTR(source []byte, key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) []byte {
+ block, _ := aes.NewCipher(key[:])
+ cipher.NewCTR(block, iv[:]).XORKeyStream(source, source)
+ return source
+}
+
+// GenAttachmentA256CTR generates a new random AES256-CTR key and IV suitable for encrypting attachments.
+func GenAttachmentA256CTR() (key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) {
+ _, err := rand.Read(key[:])
+ if err != nil {
+ panic(err)
+ }
+
+ // The last 8 bytes of the IV act as the counter in AES-CTR, which means they're left empty here
+ _, err = rand.Read(iv[:8])
+ if err != nil {
+ panic(err)
+ }
+ return
+}
+
+// GenA256CTRIV generates a random IV for AES256-CTR with the last bit set to zero.
+func GenA256CTRIV() (iv [AESCTRIVLength]byte) {
+ _, err := rand.Read(iv[:])
+ if err != nil {
+ panic(err)
+ }
+ iv[8] &= 0x7F
+ return
+}
+
+// DeriveKeysSHA256 derives an AES and a HMAC key from the given recovery key.
+func DeriveKeysSHA256(key []byte, name string) ([AESCTRKeyLength]byte, [HMACKeyLength]byte) {
+ var zeroBytes [32]byte
+
+ derivedHkdf := hkdf.New(sha256.New, key[:], zeroBytes[:], []byte(name))
+
+ var aesKey [AESCTRKeyLength]byte
+ var hmacKey [HMACKeyLength]byte
+ derivedHkdf.Read(aesKey[:])
+ derivedHkdf.Read(hmacKey[:])
+
+ return aesKey, hmacKey
+}
+
+// PBKDF2SHA512 generates a key of the given bit-length using the given passphrase, salt and iteration count.
+func PBKDF2SHA512(password []byte, salt []byte, iters int, keyLenBits int) []byte {
+ return pbkdf2.Key(password, salt, iters, keyLenBits/8, sha512.New)
+}
+
+// DecodeBase58RecoveryKey recovers the secret storage from a recovery key.
+func DecodeBase58RecoveryKey(recoveryKey string) []byte {
+ noSpaces := strings.ReplaceAll(recoveryKey, " ", "")
+ decoded := base58.Decode(noSpaces)
+ if len(decoded) != AESCTRKeyLength+3 { // AESCTRKeyLength bytes key and 3 bytes prefix / parity
+ return nil
+ }
+ var parity byte
+ for _, b := range decoded[:34] {
+ parity ^= b
+ }
+ if parity != decoded[34] || decoded[0] != 0x8B || decoded[1] != 1 {
+ return nil
+ }
+ return decoded[2:34]
+}
+
+// EncodeBase58RecoveryKey recovers the secret storage from a recovery key.
+func EncodeBase58RecoveryKey(key []byte) string {
+ var inputBytes [35]byte
+ copy(inputBytes[2:34], key[:])
+ inputBytes[0] = 0x8B
+ inputBytes[1] = 1
+
+ var parity byte
+ for _, b := range inputBytes[:34] {
+ parity ^= b
+ }
+ inputBytes[34] = parity
+ recoveryKey := base58.Encode(inputBytes[:])
+
+ var spacedKey string
+ for i, c := range recoveryKey {
+ if i > 0 && i%4 == 0 {
+ spacedKey += " "
+ }
+ spacedKey += string(c)
+ }
+ return spacedKey
+}
+
+// HMACSHA256B64 calculates the base64 of the SHA256 hmac of the input with the given key.
+func HMACSHA256B64(input []byte, hmacKey [HMACKeyLength]byte) string {
+ h := hmac.New(sha256.New, hmacKey[:])
+ h.Write(input)
+ return base64.StdEncoding.EncodeToString(h.Sum(nil))
+}