diff options
Diffstat (limited to 'vendor/maunium.net/go/mautrix/crypto')
28 files changed, 2505 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go b/vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go new file mode 100644 index 0000000..d69a5f4 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go @@ -0,0 +1,60 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 aescbc + +import ( + "crypto/aes" + "crypto/cipher" + + "maunium.net/go/mautrix/crypto/pkcs7" +) + +// Encrypt encrypts the plaintext with the key and IV. The IV length must be +// equal to the AES block size. +// +// This function might mutate the plaintext. +func Encrypt(key, iv, plaintext []byte) ([]byte, error) { + if len(key) == 0 { + return nil, ErrNoKeyProvided + } + if len(iv) != aes.BlockSize { + return nil, ErrIVNotBlockSize + } + plaintext = pkcs7.Pad(plaintext, aes.BlockSize) + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + cipher.NewCBCEncrypter(block, iv).CryptBlocks(plaintext, plaintext) + return plaintext, nil +} + +// Decrypt decrypts the ciphertext with the key and IV. The IV length must be +// equal to the block size. +// +// This function mutates the ciphertext. +func Decrypt(key, iv, ciphertext []byte) ([]byte, error) { + if len(key) == 0 { + return nil, ErrNoKeyProvided + } + if len(iv) != aes.BlockSize { + return nil, ErrIVNotBlockSize + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + if len(ciphertext) < aes.BlockSize { + return nil, ErrNotMultipleBlockSize + } + + cipher.NewCBCDecrypter(block, iv).CryptBlocks(ciphertext, ciphertext) + return pkcs7.Unpad(ciphertext), nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go b/vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go new file mode 100644 index 0000000..f3d2d7c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go @@ -0,0 +1,15 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 aescbc + +import "errors" + +var ( + ErrNoKeyProvided = errors.New("no key") + ErrIVNotBlockSize = errors.New("IV length does not match AES block size") + ErrNotMultipleBlockSize = errors.New("ciphertext length is not a multiple of the AES block size") +) 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, + } +} diff --git a/vendor/maunium.net/go/mautrix/crypto/backup/encryptedsessiondata.go b/vendor/maunium.net/go/mautrix/crypto/backup/encryptedsessiondata.go new file mode 100644 index 0000000..ec551db --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/backup/encryptedsessiondata.go @@ -0,0 +1,131 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 backup + +import ( + "bytes" + "crypto/ecdh" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/json" + "errors" + + "go.mau.fi/util/jsonbytes" + "golang.org/x/crypto/hkdf" + + "maunium.net/go/mautrix/crypto/aescbc" +) + +var ErrInvalidMAC = errors.New("invalid MAC") + +// EncryptedSessionData is the encrypted session_data field of a key backup as +// defined in [Section 11.12.3.2.2 of the Spec]. +// +// The type parameter T represents the format of the session data contained in +// the encrypted payload. +// +// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2 +type EncryptedSessionData[T any] struct { + Ciphertext jsonbytes.UnpaddedBytes `json:"ciphertext"` + Ephemeral EphemeralKey `json:"ephemeral"` + MAC jsonbytes.UnpaddedBytes `json:"mac"` +} + +func calculateEncryptionParameters(sharedSecret []byte) (key, macKey, iv []byte, err error) { + hkdfReader := hkdf.New(sha256.New, sharedSecret, nil, nil) + encryptionParams := make([]byte, 80) + _, err = hkdfReader.Read(encryptionParams) + if err != nil { + return nil, nil, nil, err + } + + return encryptionParams[:32], encryptionParams[32:64], encryptionParams[64:], nil +} + +// calculateCompatMAC calculates the MAC as described in step 5 of according to +// [Section 11.12.3.2.2] of the Spec which was updated in spec version 1.10 to +// reflect what is actually implemented in libolm and Vodozemac. +// +// Libolm implemented the MAC functionality incorrectly. The MAC is computed +// over an empty string rather than the ciphertext. Vodozemac implemented this +// functionality the same way as libolm for compatibility. In version 1.10 of +// the spec, the description of step 5 was updated to reflect the de-facto +// standard of libolm and Vodozemac. +// +// [Section 11.12.3.2.2]: https://spec.matrix.org/v1.11/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2 +func calculateCompatMAC(macKey []byte) []byte { + hash := hmac.New(sha256.New, macKey) + return hash.Sum(nil)[:8] +} + +// EncryptSessionData encrypts the given session data with the given recovery +// key as defined in [Section 11.12.3.2.2 of the Spec]. +// +// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2 +func EncryptSessionData[T any](backupKey *MegolmBackupKey, sessionData T) (*EncryptedSessionData[T], error) { + sessionJSON, err := json.Marshal(sessionData) + if err != nil { + return nil, err + } + + ephemeralKey, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + + sharedSecret, err := ephemeralKey.ECDH(backupKey.PublicKey()) + if err != nil { + return nil, err + } + + key, macKey, iv, err := calculateEncryptionParameters(sharedSecret) + if err != nil { + return nil, err + } + + ciphertext, err := aescbc.Encrypt(key, iv, sessionJSON) + if err != nil { + return nil, err + } + + return &EncryptedSessionData[T]{ + Ciphertext: ciphertext, + Ephemeral: EphemeralKey{ephemeralKey.PublicKey()}, + MAC: calculateCompatMAC(macKey), + }, nil +} + +// Decrypt decrypts the [EncryptedSessionData] into a *T using the recovery key +// by reversing the process described in [Section 11.12.3.2.2 of the Spec]. +// +// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2 +func (esd *EncryptedSessionData[T]) Decrypt(backupKey *MegolmBackupKey) (*T, error) { + sharedSecret, err := backupKey.ECDH(esd.Ephemeral.PublicKey) + if err != nil { + return nil, err + } + + key, macKey, iv, err := calculateEncryptionParameters(sharedSecret) + if err != nil { + return nil, err + } + + // Verify the MAC before decrypting. + if !bytes.Equal(calculateCompatMAC(macKey), esd.MAC) { + return nil, ErrInvalidMAC + } + + plaintext, err := aescbc.Decrypt(key, iv, esd.Ciphertext) + if err != nil { + return nil, err + } + + var sessionData T + err = json.Unmarshal(plaintext, &sessionData) + return &sessionData, err +} diff --git a/vendor/maunium.net/go/mautrix/crypto/backup/ephemeralkey.go b/vendor/maunium.net/go/mautrix/crypto/backup/ephemeralkey.go new file mode 100644 index 0000000..e481e7a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/backup/ephemeralkey.go @@ -0,0 +1,41 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 backup + +import ( + "crypto/ecdh" + "encoding/base64" + "encoding/json" +) + +// EphemeralKey is a wrapper around an ECDH X25519 public key that implements +// JSON marshalling and unmarshalling. +type EphemeralKey struct { + *ecdh.PublicKey +} + +func (k *EphemeralKey) MarshalJSON() ([]byte, error) { + if k == nil || k.PublicKey == nil { + return json.Marshal(nil) + } + return json.Marshal(base64.RawStdEncoding.EncodeToString(k.Bytes())) +} + +func (k *EphemeralKey) UnmarshalJSON(data []byte) error { + var keyStr string + err := json.Unmarshal(data, &keyStr) + if err != nil { + return err + } + + keyBytes, err := base64.RawStdEncoding.DecodeString(keyStr) + if err != nil { + return err + } + k.PublicKey, err = ecdh.X25519().NewPublicKey(keyBytes) + return err +} diff --git a/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackup.go b/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackup.go new file mode 100644 index 0000000..71b8e88 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackup.go @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 backup + +import ( + "maunium.net/go/mautrix/crypto/signatures" + "maunium.net/go/mautrix/id" +) + +// MegolmAuthData is the auth_data when the key backup is created with +// the [id.KeyBackupAlgorithmMegolmBackupV1] algorithm as defined in +// [Section 11.12.3.2.2 of the Spec]. +// +// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2 +type MegolmAuthData struct { + PublicKey id.Ed25519 `json:"public_key"` + Signatures signatures.Signatures `json:"signatures"` +} + +type SenderClaimedKeys struct { + Ed25519 id.Ed25519 `json:"ed25519"` +} + +// MegolmSessionData is the decrypted session_data when the key backup is created +// with the [id.KeyBackupAlgorithmMegolmBackupV1] algorithm as defined in +// [Section 11.12.3.2.2 of the Spec]. +// +// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2 +type MegolmSessionData struct { + Algorithm id.Algorithm `json:"algorithm"` + ForwardingKeyChain []string `json:"forwarding_curve25519_key_chain"` + SenderClaimedKeys SenderClaimedKeys `json:"sender_claimed_keys"` + SenderKey id.SenderKey `json:"sender_key"` + SessionKey string `json:"session_key"` +} diff --git a/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackupkey.go b/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackupkey.go new file mode 100644 index 0000000..8f23d10 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackupkey.go @@ -0,0 +1,34 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 backup + +import ( + "crypto/ecdh" + "crypto/rand" +) + +// MegolmBackupKey is a wrapper around an ECDH X25519 private key that is used +// to decrypt a megolm key backup. +type MegolmBackupKey struct { + *ecdh.PrivateKey +} + +func NewMegolmBackupKey() (*MegolmBackupKey, error) { + key, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + return &MegolmBackupKey{key}, nil +} + +func MegolmBackupKeyFromBytes(bytes []byte) (*MegolmBackupKey, error) { + key, err := ecdh.X25519().NewPrivateKey(bytes) + if err != nil { + return nil, err + } + return &MegolmBackupKey{key}, nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md new file mode 100644 index 0000000..da9d71f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md @@ -0,0 +1,6 @@ +# canonicaljson +This is a Go package to produce Matrix [Canonical JSON](https://matrix.org/docs/spec/appendices#canonical-json). +It is essentially just [json.go](https://github.com/matrix-org/gomatrixserverlib/blob/master/json.go) +from gomatrixserverlib without all the other files that are completely useless for non-server use cases. + +The original project is licensed under the Apache 2.0 license. diff --git a/vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go new file mode 100644 index 0000000..fd296e6 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go @@ -0,0 +1,257 @@ +/* Copyright 2016-2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package canonicaljson + +import ( + "encoding/binary" + "fmt" + "sort" + "unicode/utf8" + + "github.com/tidwall/gjson" +) + +// CanonicalJSON re-encodes the JSON in a canonical encoding. The encoding is +// the shortest possible encoding using integer values with sorted object keys. +// https://matrix.org/docs/spec/appendices#canonical-json +func CanonicalJSON(input []byte) ([]byte, error) { + if !gjson.Valid(string(input)) { + return nil, fmt.Errorf("invalid json") + } + + return CanonicalJSONAssumeValid(input), nil +} + +// CanonicalJSONAssumeValid is the same as CanonicalJSON, but assumes the +// input is valid JSON +func CanonicalJSONAssumeValid(input []byte) []byte { + input = CompactJSON(input, make([]byte, 0, len(input))) + return SortJSON(input, make([]byte, 0, len(input))) +} + +// SortJSON reencodes the JSON with the object keys sorted by lexicographically +// by codepoint. The input must be valid JSON. +func SortJSON(input, output []byte) []byte { + result := gjson.ParseBytes(input) + + return sortJSONValue(result, input, output) +} + +// sortJSONValue takes a gjson.Result and sorts it. inputJSON must be the +// raw JSON bytes that gjson.Result points to. +func sortJSONValue(input gjson.Result, inputJSON, output []byte) []byte { + if input.IsArray() { + return sortJSONArray(input, inputJSON, output) + } + + if input.IsObject() { + return sortJSONObject(input, inputJSON, output) + } + + // If its neither an object nor an array then there is no sub structure + // to sort, so just append the raw bytes. + return append(output, input.Raw...) +} + +// sortJSONArray takes a gjson.Result and sorts it, assuming its an array. +// inputJSON must be the raw JSON bytes that gjson.Result points to. +func sortJSONArray(input gjson.Result, inputJSON, output []byte) []byte { + sep := byte('[') + + // Iterate over each value in the array and sort it. + input.ForEach(func(_, value gjson.Result) bool { + output = append(output, sep) + sep = ',' + output = sortJSONValue(value, inputJSON, output) + return true // keep iterating + }) + + if sep == '[' { + // If sep is still '[' then the array was empty and we never wrote the + // initial '[', so we write it now along with the closing ']'. + output = append(output, '[', ']') + } else { + // Otherwise we end the array by writing a single ']' + output = append(output, ']') + } + return output +} + +// sortJSONObject takes a gjson.Result and sorts it, assuming its an object. +// inputJSON must be the raw JSON bytes that gjson.Result points to. +func sortJSONObject(input gjson.Result, inputJSON, output []byte) []byte { + type entry struct { + key string // The parsed key string + rawKey string // The raw, unparsed key JSON string + value gjson.Result + } + + var entries []entry + + // Iterate over each key/value pair and add it to a slice + // that we can sort + input.ForEach(func(key, value gjson.Result) bool { + entries = append(entries, entry{ + key: key.String(), + rawKey: key.Raw, + value: value, + }) + return true // keep iterating + }) + + // Sort the slice based on the *parsed* key + sort.Slice(entries, func(a, b int) bool { + return entries[a].key < entries[b].key + }) + + sep := byte('{') + + for _, entry := range entries { + output = append(output, sep) + sep = ',' + + // Append the raw unparsed JSON key, *not* the parsed key + output = append(output, entry.rawKey...) + output = append(output, ':') + output = sortJSONValue(entry.value, inputJSON, output) + } + if sep == '{' { + // If sep is still '{' then the object was empty and we never wrote the + // initial '{', so we write it now along with the closing '}'. + output = append(output, '{', '}') + } else { + // Otherwise we end the object by writing a single '}' + output = append(output, '}') + } + return output +} + +// CompactJSON makes the encoded JSON as small as possible by removing +// whitespace and unneeded unicode escapes +func CompactJSON(input, output []byte) []byte { + var i int + for i < len(input) { + c := input[i] + i++ + // The valid whitespace characters are all less than or equal to SPACE 0x20. + // The valid non-white characters are all greater than SPACE 0x20. + // So we can check for whitespace by comparing against SPACE 0x20. + if c <= ' ' { + // Skip over whitespace. + continue + } + // Add the non-whitespace character to the output. + output = append(output, c) + if c == '"' { + // We are inside a string. + for i < len(input) { + c = input[i] + i++ + // Check if this is an escape sequence. + if c == '\\' { + escape := input[i] + i++ + if escape == 'u' { + // If this is a unicode escape then we need to handle it specially + output, i = compactUnicodeEscape(input, output, i) + } else if escape == '/' { + // JSON does not require escaping '/', but allows encoders to escape it as a special case. + // Since the escape isn't required we remove it. + output = append(output, escape) + } else { + // All other permitted escapes are single charater escapes that are already in their shortest form. + output = append(output, '\\', escape) + } + } else { + output = append(output, c) + } + if c == '"' { + break + } + } + } + } + return output +} + +// compactUnicodeEscape unpacks a 4 byte unicode escape starting at index. +// If the escape is a surrogate pair then decode the 6 byte \uXXXX escape +// that follows. Returns the output slice and a new input index. +func compactUnicodeEscape(input, output []byte, index int) ([]byte, int) { + const ( + ESCAPES = "uuuuuuuubtnufruuuuuuuuuuuuuuuuuu" + HEX = "0123456789ABCDEF" + ) + // If there aren't enough bytes to decode the hex escape then return. + if len(input)-index < 4 { + return output, len(input) + } + // Decode the 4 hex digits. + c := readHexDigits(input[index:]) + index += 4 + if c < ' ' { + // If the character is less than SPACE 0x20 then it will need escaping. + escape := ESCAPES[c] + output = append(output, '\\', escape) + if escape == 'u' { + output = append(output, '0', '0', byte('0'+(c>>4)), HEX[c&0xF]) + } + } else if c == '\\' || c == '"' { + // Otherwise the character only needs escaping if it is a QUOTE '"' or BACKSLASH '\\'. + output = append(output, '\\', byte(c)) + } else if c < 0xD800 || c >= 0xE000 { + // If the character isn't a surrogate pair then encoded it directly as UTF-8. + var buffer [4]byte + n := utf8.EncodeRune(buffer[:], rune(c)) + output = append(output, buffer[:n]...) + } else { + // Otherwise the escaped character was the first part of a UTF-16 style surrogate pair. + // The next 6 bytes MUST be a '\uXXXX'. + // If there aren't enough bytes to decode the hex escape then return. + if len(input)-index < 6 { + return output, len(input) + } + // Decode the 4 hex digits from the '\uXXXX'. + surrogate := readHexDigits(input[index+2:]) + index += 6 + // Reconstruct the UCS4 codepoint from the surrogates. + codepoint := 0x10000 + (((c & 0x3FF) << 10) | (surrogate & 0x3FF)) + // Encode the charater as UTF-8. + var buffer [4]byte + n := utf8.EncodeRune(buffer[:], rune(codepoint)) + output = append(output, buffer[:n]...) + } + return output, index +} + +// Read 4 hex digits from the input slice. +// Taken from https://github.com/NegativeMjark/indolentjson-rust/blob/8b959791fe2656a88f189c5d60d153be05fe3deb/src/readhex.rs#L21 +func readHexDigits(input []byte) uint32 { + hex := binary.BigEndian.Uint32(input) + // subtract '0' + hex -= 0x30303030 + // strip the higher bits, maps 'a' => 'A' + hex &= 0x1F1F1F1F + mask := hex & 0x10101010 + // subtract 'A' - 10 - '9' - 9 = 7 from the letters. + hex -= mask >> 1 + hex += mask >> 4 + // collect the nibbles + hex |= hex >> 4 + hex &= 0xFF00FF + hex |= hex >> 8 + return hex & 0xFFFF +} diff --git a/vendor/maunium.net/go/mautrix/crypto/ed25519/ed25519.go b/vendor/maunium.net/go/mautrix/crypto/ed25519/ed25519.go new file mode 100644 index 0000000..327cbb3 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/ed25519/ed25519.go @@ -0,0 +1,302 @@ +// Copyright 2024 Sumner Evans. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package ed25519 implements the Ed25519 signature algorithm. See +// https://ed25519.cr.yp.to/. +// +// This package stores the private key in the NaCl format, which is a different +// format than that used by the [crypto/ed25519] package in the standard +// library. +// +// This picture will help with the rest of the explanation: +// https://blog.mozilla.org/warner/files/2011/11/key-formats.png +// +// The private key in the [crypto/ed25519] package is a 64-byte value where the +// first 32-bytes are the seed and the last 32-bytes are the public key. +// +// The private key in this package is stored in the NaCl format. That is, the +// left 32-bytes are the private scalar A and the right 32-bytes are the right +// half of the SHA512 result. +// +// The contents of this package are mostly copied from the standard library, +// and as such the source code is licensed under the BSD license of the +// standard library implementation. +// +// Other notable changes from the standard library include: +// +// - The Seed function of the standard library is not implemented in this +// package because there is no way to recover the seed after hashing it. +package ed25519 + +import ( + "crypto" + "crypto/ed25519" + cryptorand "crypto/rand" + "crypto/sha512" + "crypto/subtle" + "errors" + "io" + "strconv" + + "filippo.io/edwards25519" +) + +const ( + // PublicKeySize is the size, in bytes, of public keys as used in this package. + PublicKeySize = 32 + // PrivateKeySize is the size, in bytes, of private keys as used in this package. + PrivateKeySize = 64 + // SignatureSize is the size, in bytes, of signatures generated and verified by this package. + SignatureSize = 64 + // SeedSize is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032. + SeedSize = 32 +) + +// PublicKey is the type of Ed25519 public keys. +type PublicKey []byte + +// Any methods implemented on PublicKey might need to also be implemented on +// PrivateKey, as the latter embeds the former and will expose its methods. + +// Equal reports whether pub and x have the same value. +func (pub PublicKey) Equal(x crypto.PublicKey) bool { + switch x := x.(type) { + case PublicKey: + return subtle.ConstantTimeCompare(pub, x) == 1 + case ed25519.PublicKey: + return subtle.ConstantTimeCompare(pub, x) == 1 + default: + return false + } +} + +// PrivateKey is the type of Ed25519 private keys. It implements [crypto.Signer]. +type PrivateKey []byte + +// Public returns the [PublicKey] corresponding to priv. +// +// This method differs from the standard library because it calculates the +// public key instead of returning the right half of the private key (which +// contains the public key in the standard library). +func (priv PrivateKey) Public() crypto.PublicKey { + s, err := edwards25519.NewScalar().SetBytesWithClamping(priv[:32]) + if err != nil { + panic("ed25519: internal error: setting scalar failed") + } + return (&edwards25519.Point{}).ScalarBaseMult(s).Bytes() +} + +// Equal reports whether priv and x have the same value. +func (priv PrivateKey) Equal(x crypto.PrivateKey) bool { + // TODO do we have any need to check equality with standard library ed25519 + // private keys? + xx, ok := x.(PrivateKey) + if !ok { + return false + } + return subtle.ConstantTimeCompare(priv, xx) == 1 +} + +// Sign signs the given message with priv. rand is ignored and can be nil. +// +// If opts.HashFunc() is [crypto.SHA512], the pre-hashed variant Ed25519ph is used +// and message is expected to be a SHA-512 hash, otherwise opts.HashFunc() must +// be [crypto.Hash](0) and the message must not be hashed, as Ed25519 performs two +// passes over messages to be signed. +// +// A value of type [Options] can be used as opts, or crypto.Hash(0) or +// crypto.SHA512 directly to select plain Ed25519 or Ed25519ph, respectively. +func (priv PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) { + hash := opts.HashFunc() + context := "" + if opts, ok := opts.(*Options); ok { + context = opts.Context + } + switch { + case hash == crypto.SHA512: // Ed25519ph + if l := len(message); l != sha512.Size { + return nil, errors.New("ed25519: bad Ed25519ph message hash length: " + strconv.Itoa(l)) + } + if l := len(context); l > 255 { + return nil, errors.New("ed25519: bad Ed25519ph context length: " + strconv.Itoa(l)) + } + signature := make([]byte, SignatureSize) + sign(signature, priv, message, domPrefixPh, context) + return signature, nil + case hash == crypto.Hash(0) && context != "": // Ed25519ctx + if l := len(context); l > 255 { + return nil, errors.New("ed25519: bad Ed25519ctx context length: " + strconv.Itoa(l)) + } + signature := make([]byte, SignatureSize) + sign(signature, priv, message, domPrefixCtx, context) + return signature, nil + case hash == crypto.Hash(0): // Ed25519 + return Sign(priv, message), nil + default: + return nil, errors.New("ed25519: expected opts.HashFunc() zero (unhashed message, for standard Ed25519) or SHA-512 (for Ed25519ph)") + } +} + +// Options can be used with [PrivateKey.Sign] or [VerifyWithOptions] +// to select Ed25519 variants. +type Options struct { + // Hash can be zero for regular Ed25519, or crypto.SHA512 for Ed25519ph. + Hash crypto.Hash + + // Context, if not empty, selects Ed25519ctx or provides the context string + // for Ed25519ph. It can be at most 255 bytes in length. + Context string +} + +// HashFunc returns o.Hash. +func (o *Options) HashFunc() crypto.Hash { return o.Hash } + +// GenerateKey generates a public/private key pair using entropy from rand. +// If rand is nil, [crypto/rand.Reader] will be used. +// +// The output of this function is deterministic, and equivalent to reading +// [SeedSize] bytes from rand, and passing them to [NewKeyFromSeed]. +func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error) { + if rand == nil { + rand = cryptorand.Reader + } + + seed := make([]byte, SeedSize) + if _, err := io.ReadFull(rand, seed); err != nil { + return nil, nil, err + } + + privateKey := NewKeyFromSeed(seed) + return PublicKey(privateKey.Public().([]byte)), privateKey, nil +} + +// NewKeyFromSeed calculates a private key from a seed. It will panic if +// len(seed) is not [SeedSize]. This function is provided for interoperability +// with RFC 8032. RFC 8032's private keys correspond to seeds in this +// package. +func NewKeyFromSeed(seed []byte) PrivateKey { + // Outline the function body so that the returned key can be stack-allocated. + privateKey := make([]byte, PrivateKeySize) + newKeyFromSeed(privateKey, seed) + return privateKey +} + +func newKeyFromSeed(privateKey, seed []byte) { + if l := len(seed); l != SeedSize { + panic("ed25519: bad seed length: " + strconv.Itoa(l)) + } + + h := sha512.Sum512(seed) + + // Apply clamping to get A in the left half, and leave the right half + // as-is. This gets the private key into the NaCl format. + h[0] &= 248 + h[31] &= 63 + h[31] |= 64 + copy(privateKey, h[:]) +} + +// Sign signs the message with privateKey and returns a signature. It will +// panic if len(privateKey) is not [PrivateKeySize]. +func Sign(privateKey PrivateKey, message []byte) []byte { + // Outline the function body so that the returned signature can be + // stack-allocated. + signature := make([]byte, SignatureSize) + sign(signature, privateKey, message, domPrefixPure, "") + return signature +} + +// Domain separation prefixes used to disambiguate Ed25519/Ed25519ph/Ed25519ctx. +// See RFC 8032, Section 2 and Section 5.1. +const ( + // domPrefixPure is empty for pure Ed25519. + domPrefixPure = "" + // domPrefixPh is dom2(phflag=1) for Ed25519ph. It must be followed by the + // uint8-length prefixed context. + domPrefixPh = "SigEd25519 no Ed25519 collisions\x01" + // domPrefixCtx is dom2(phflag=0) for Ed25519ctx. It must be followed by the + // uint8-length prefixed context. + domPrefixCtx = "SigEd25519 no Ed25519 collisions\x00" +) + +func sign(signature []byte, privateKey PrivateKey, message []byte, domPrefix, context string) { + if l := len(privateKey); l != PrivateKeySize { + panic("ed25519: bad private key length: " + strconv.Itoa(l)) + } + // We have to extract the public key from the private key. + publicKey := privateKey.Public().([]byte) + // The private key is already the hashed value of the seed. + h := privateKey + + s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + if err != nil { + panic("ed25519: internal error: setting scalar failed") + } + prefix := h[32:] + + mh := sha512.New() + if domPrefix != domPrefixPure { + mh.Write([]byte(domPrefix)) + mh.Write([]byte{byte(len(context))}) + mh.Write([]byte(context)) + } + mh.Write(prefix) + mh.Write(message) + messageDigest := make([]byte, 0, sha512.Size) + messageDigest = mh.Sum(messageDigest) + r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest) + if err != nil { + panic("ed25519: internal error: setting scalar failed") + } + + R := (&edwards25519.Point{}).ScalarBaseMult(r) + + kh := sha512.New() + if domPrefix != domPrefixPure { + kh.Write([]byte(domPrefix)) + kh.Write([]byte{byte(len(context))}) + kh.Write([]byte(context)) + } + kh.Write(R.Bytes()) + kh.Write(publicKey) + kh.Write(message) + hramDigest := make([]byte, 0, sha512.Size) + hramDigest = kh.Sum(hramDigest) + k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest) + if err != nil { + panic("ed25519: internal error: setting scalar failed") + } + + S := edwards25519.NewScalar().MultiplyAdd(k, s, r) + + copy(signature[:32], R.Bytes()) + copy(signature[32:], S.Bytes()) +} + +// Verify reports whether sig is a valid signature of message by publicKey. It +// will panic if len(publicKey) is not [PublicKeySize]. +// +// This is just a wrapper around [ed25519.Verify] from the standard library. +func Verify(publicKey PublicKey, message, sig []byte) bool { + return ed25519.Verify(ed25519.PublicKey(publicKey), message, sig) +} + +// VerifyWithOptions reports whether sig is a valid signature of message by +// publicKey. A valid signature is indicated by returning a nil error. It will +// panic if len(publicKey) is not [PublicKeySize]. +// +// If opts.Hash is [crypto.SHA512], the pre-hashed variant Ed25519ph is used and +// message is expected to be a SHA-512 hash, otherwise opts.Hash must be +// [crypto.Hash](0) and the message must not be hashed, as Ed25519 performs two +// passes over messages to be signed. +// +// This is just a wrapper around [ed25519.VerifyWithOptions] from the standard +// library. +func VerifyWithOptions(publicKey PublicKey, message, sig []byte, opts *Options) error { + return ed25519.VerifyWithOptions(ed25519.PublicKey(publicKey), message, sig, &ed25519.Options{ + Hash: opts.Hash, + Context: opts.Context, + }) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go new file mode 100644 index 0000000..1c182ca --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go @@ -0,0 +1,186 @@ +package crypto + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + "golang.org/x/crypto/curve25519" + + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/olm" + "maunium.net/go/mautrix/id" +) + +const ( + Curve25519KeyLength = curve25519.ScalarSize //The length of the private key. + curve25519PubKeyLength = 32 +) + +// Curve25519GenerateKey creates a new curve25519 key pair. If reader is nil, the random data is taken from crypto/rand. +func Curve25519GenerateKey(reader io.Reader) (Curve25519KeyPair, error) { + privateKeyByte := make([]byte, Curve25519KeyLength) + if reader == nil { + _, err := rand.Read(privateKeyByte) + if err != nil { + return Curve25519KeyPair{}, err + } + } else { + _, err := reader.Read(privateKeyByte) + if err != nil { + return Curve25519KeyPair{}, err + } + } + + privateKey := Curve25519PrivateKey(privateKeyByte) + + publicKey, err := privateKey.PubKey() + if err != nil { + return Curve25519KeyPair{}, err + } + return Curve25519KeyPair{ + PrivateKey: Curve25519PrivateKey(privateKey), + PublicKey: Curve25519PublicKey(publicKey), + }, nil +} + +// Curve25519GenerateFromPrivate creates a new curve25519 key pair with the private key given. +func Curve25519GenerateFromPrivate(private Curve25519PrivateKey) (Curve25519KeyPair, error) { + publicKey, err := private.PubKey() + if err != nil { + return Curve25519KeyPair{}, err + } + return Curve25519KeyPair{ + PrivateKey: private, + PublicKey: Curve25519PublicKey(publicKey), + }, nil +} + +// Curve25519KeyPair stores both parts of a curve25519 key. +type Curve25519KeyPair struct { + PrivateKey Curve25519PrivateKey `json:"private,omitempty"` + PublicKey Curve25519PublicKey `json:"public,omitempty"` +} + +// B64Encoded returns a base64 encoded string of the public key. +func (c Curve25519KeyPair) B64Encoded() id.Curve25519 { + return c.PublicKey.B64Encoded() +} + +// SharedSecret returns the shared secret between the key pair and the given public key. +func (c Curve25519KeyPair) SharedSecret(pubKey Curve25519PublicKey) ([]byte, error) { + return c.PrivateKey.SharedSecret(pubKey) +} + +// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c Curve25519KeyPair) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle curve25519 key pair: %w", olm.ErrValueTooShort) + } + written, err := c.PublicKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle curve25519 key pair: %w", err) + } + if len(c.PrivateKey) != Curve25519KeyLength { + written += libolmpickle.PickleBytes(make([]byte, Curve25519KeyLength), target[written:]) + } else { + written += libolmpickle.PickleBytes(c.PrivateKey, target[written:]) + } + return written, nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the key pair accordingly. It returns the number of bytes read. +func (c *Curve25519KeyPair) UnpickleLibOlm(value []byte) (int, error) { + //unpickle PubKey + read, err := c.PublicKey.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + //unpickle PrivateKey + privKey, readPriv, err := libolmpickle.UnpickleBytes(value[read:], Curve25519KeyLength) + if err != nil { + return read, err + } + c.PrivateKey = privKey + return read + readPriv, nil +} + +// PickleLen returns the number of bytes the pickled key pair will have. +func (c Curve25519KeyPair) PickleLen() int { + lenPublic := c.PublicKey.PickleLen() + var lenPrivate int + if len(c.PrivateKey) != Curve25519KeyLength { + lenPrivate = libolmpickle.PickleBytesLen(make([]byte, Curve25519KeyLength)) + } else { + lenPrivate = libolmpickle.PickleBytesLen(c.PrivateKey) + } + return lenPublic + lenPrivate +} + +// Curve25519PrivateKey represents the private key for curve25519 usage +type Curve25519PrivateKey []byte + +// Equal compares the private key to the given private key. +func (c Curve25519PrivateKey) Equal(x Curve25519PrivateKey) bool { + return bytes.Equal(c, x) +} + +// PubKey returns the public key derived from the private key. +func (c Curve25519PrivateKey) PubKey() (Curve25519PublicKey, error) { + publicKey, err := curve25519.X25519(c, curve25519.Basepoint) + if err != nil { + return nil, err + } + return publicKey, nil +} + +// SharedSecret returns the shared secret between the private key and the given public key. +func (c Curve25519PrivateKey) SharedSecret(pubKey Curve25519PublicKey) ([]byte, error) { + return curve25519.X25519(c, pubKey) +} + +// Curve25519PublicKey represents the public key for curve25519 usage +type Curve25519PublicKey []byte + +// Equal compares the public key to the given public key. +func (c Curve25519PublicKey) Equal(x Curve25519PublicKey) bool { + return bytes.Equal(c, x) +} + +// B64Encoded returns a base64 encoded string of the public key. +func (c Curve25519PublicKey) B64Encoded() id.Curve25519 { + return id.Curve25519(base64.RawStdEncoding.EncodeToString(c)) +} + +// PickleLibOlm encodes the public key into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c Curve25519PublicKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle curve25519 public key: %w", olm.ErrValueTooShort) + } + if len(c) != curve25519PubKeyLength { + return libolmpickle.PickleBytes(make([]byte, curve25519PubKeyLength), target), nil + } + return libolmpickle.PickleBytes(c, target), nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the public key accordingly. It returns the number of bytes read. +func (c *Curve25519PublicKey) UnpickleLibOlm(value []byte) (int, error) { + unpickled, readBytes, err := libolmpickle.UnpickleBytes(value, curve25519PubKeyLength) + if err != nil { + return 0, err + } + *c = unpickled + return readBytes, nil +} + +// PickleLen returns the number of bytes the pickled public key will have. +func (c Curve25519PublicKey) PickleLen() int { + if len(c) != curve25519PubKeyLength { + return libolmpickle.PickleBytesLen(make([]byte, curve25519PubKeyLength)) + } + return libolmpickle.PickleBytesLen(c) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/doc.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/doc.go new file mode 100644 index 0000000..5bdb01d --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/doc.go @@ -0,0 +1,2 @@ +// Package crpyto provides the nessesary encryption methods for olm/megolm +package crypto diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go new file mode 100644 index 0000000..57fc25f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go @@ -0,0 +1,184 @@ +package crypto + +import ( + "encoding/base64" + "fmt" + "io" + + "maunium.net/go/mautrix/crypto/ed25519" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/olm" + "maunium.net/go/mautrix/id" +) + +const ( + ED25519SignatureSize = ed25519.SignatureSize //The length of a signature +) + +// Ed25519GenerateKey creates a new ed25519 key pair. If reader is nil, the random data is taken from crypto/rand. +func Ed25519GenerateKey(reader io.Reader) (Ed25519KeyPair, error) { + publicKey, privateKey, err := ed25519.GenerateKey(reader) + if err != nil { + return Ed25519KeyPair{}, err + } + return Ed25519KeyPair{ + PrivateKey: Ed25519PrivateKey(privateKey), + PublicKey: Ed25519PublicKey(publicKey), + }, nil +} + +// Ed25519GenerateFromPrivate creates a new ed25519 key pair with the private key given. +func Ed25519GenerateFromPrivate(privKey Ed25519PrivateKey) Ed25519KeyPair { + return Ed25519KeyPair{ + PrivateKey: privKey, + PublicKey: privKey.PubKey(), + } +} + +// Ed25519GenerateFromSeed creates a new ed25519 key pair with a given seed. +func Ed25519GenerateFromSeed(seed []byte) Ed25519KeyPair { + privKey := Ed25519PrivateKey(ed25519.NewKeyFromSeed(seed)) + return Ed25519KeyPair{ + PrivateKey: privKey, + PublicKey: privKey.PubKey(), + } +} + +// Ed25519KeyPair stores both parts of a ed25519 key. +type Ed25519KeyPair struct { + PrivateKey Ed25519PrivateKey `json:"private,omitempty"` + PublicKey Ed25519PublicKey `json:"public,omitempty"` +} + +// B64Encoded returns a base64 encoded string of the public key. +func (c Ed25519KeyPair) B64Encoded() id.Ed25519 { + return id.Ed25519(base64.RawStdEncoding.EncodeToString(c.PublicKey)) +} + +// Sign returns the signature for the message. +func (c Ed25519KeyPair) Sign(message []byte) []byte { + return c.PrivateKey.Sign(message) +} + +// Verify checks the signature of the message against the givenSignature +func (c Ed25519KeyPair) Verify(message, givenSignature []byte) bool { + return c.PublicKey.Verify(message, givenSignature) +} + +// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c Ed25519KeyPair) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle ed25519 key pair: %w", olm.ErrValueTooShort) + } + written, err := c.PublicKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle ed25519 key pair: %w", err) + } + + if len(c.PrivateKey) != ed25519.PrivateKeySize { + written += libolmpickle.PickleBytes(make([]byte, ed25519.PrivateKeySize), target[written:]) + } else { + written += libolmpickle.PickleBytes(c.PrivateKey, target[written:]) + } + return written, nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the key pair accordingly. It returns the number of bytes read. +func (c *Ed25519KeyPair) UnpickleLibOlm(value []byte) (int, error) { + //unpickle PubKey + read, err := c.PublicKey.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + //unpickle PrivateKey + privKey, readPriv, err := libolmpickle.UnpickleBytes(value[read:], ed25519.PrivateKeySize) + if err != nil { + return read, err + } + c.PrivateKey = privKey + return read + readPriv, nil +} + +// PickleLen returns the number of bytes the pickled key pair will have. +func (c Ed25519KeyPair) PickleLen() int { + lenPublic := c.PublicKey.PickleLen() + var lenPrivate int + if len(c.PrivateKey) != ed25519.PrivateKeySize { + lenPrivate = libolmpickle.PickleBytesLen(make([]byte, ed25519.PrivateKeySize)) + } else { + lenPrivate = libolmpickle.PickleBytesLen(c.PrivateKey) + } + return lenPublic + lenPrivate +} + +// Curve25519PrivateKey represents the private key for ed25519 usage. This is just a wrapper. +type Ed25519PrivateKey ed25519.PrivateKey + +// Equal compares the private key to the given private key. +func (c Ed25519PrivateKey) Equal(x Ed25519PrivateKey) bool { + return ed25519.PrivateKey(c).Equal(ed25519.PrivateKey(x)) +} + +// PubKey returns the public key derived from the private key. +func (c Ed25519PrivateKey) PubKey() Ed25519PublicKey { + publicKey := ed25519.PrivateKey(c).Public() + return Ed25519PublicKey(publicKey.([]byte)) +} + +// Sign returns the signature for the message. +func (c Ed25519PrivateKey) Sign(message []byte) []byte { + signature, err := ed25519.PrivateKey(c).Sign(nil, message, &ed25519.Options{}) + if err != nil { + panic(err) + } + return signature +} + +// Ed25519PublicKey represents the public key for ed25519 usage. This is just a wrapper. +type Ed25519PublicKey ed25519.PublicKey + +// Equal compares the public key to the given public key. +func (c Ed25519PublicKey) Equal(x Ed25519PublicKey) bool { + return ed25519.PublicKey(c).Equal(ed25519.PublicKey(x)) +} + +// B64Encoded returns a base64 encoded string of the public key. +func (c Ed25519PublicKey) B64Encoded() id.Curve25519 { + return id.Curve25519(base64.RawStdEncoding.EncodeToString(c)) +} + +// Verify checks the signature of the message against the givenSignature +func (c Ed25519PublicKey) Verify(message, givenSignature []byte) bool { + return ed25519.Verify(ed25519.PublicKey(c), message, givenSignature) +} + +// PickleLibOlm encodes the public key into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c Ed25519PublicKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle ed25519 public key: %w", olm.ErrValueTooShort) + } + if len(c) != ed25519.PublicKeySize { + return libolmpickle.PickleBytes(make([]byte, ed25519.PublicKeySize), target), nil + } + return libolmpickle.PickleBytes(c, target), nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the public key accordingly. It returns the number of bytes read. +func (c *Ed25519PublicKey) UnpickleLibOlm(value []byte) (int, error) { + unpickled, readBytes, err := libolmpickle.UnpickleBytes(value, ed25519.PublicKeySize) + if err != nil { + return 0, err + } + *c = unpickled + return readBytes, nil +} + +// PickleLen returns the number of bytes the pickled public key will have. +func (c Ed25519PublicKey) PickleLen() int { + if len(c) != ed25519.PublicKeySize { + return libolmpickle.PickleBytesLen(make([]byte, ed25519.PublicKeySize)) + } + return libolmpickle.PickleBytesLen(c) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go new file mode 100644 index 0000000..8542f7c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go @@ -0,0 +1,29 @@ +package crypto + +import ( + "crypto/hmac" + "crypto/sha256" + "io" + + "golang.org/x/crypto/hkdf" +) + +// HMACSHA256 returns the hash message authentication code with SHA-256 of the input with the key. +func HMACSHA256(key, input []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(input) + return hash.Sum(nil) +} + +// SHA256 return the SHA-256 of the value. +func SHA256(value []byte) []byte { + hash := sha256.New() + hash.Write(value) + return hash.Sum(nil) +} + +// HKDFSHA256 is the key deivation function based on HMAC and returns a reader based on input. salt and info can both be nil. +// The reader can be used to read an arbitary length of bytes which are based on all parameters. +func HKDFSHA256(input, salt, info []byte) io.Reader { + return hkdf.New(sha256.New, input, salt, info) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go new file mode 100644 index 0000000..aaa253d --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go @@ -0,0 +1,95 @@ +package crypto + +import ( + "encoding/base64" + "encoding/binary" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/olm" + "maunium.net/go/mautrix/id" +) + +// OneTimeKey stores the information about a one time key. +type OneTimeKey struct { + ID uint32 `json:"id"` + Published bool `json:"published"` + Key Curve25519KeyPair `json:"key,omitempty"` +} + +// Equal compares the one time key to the given one. +func (otk OneTimeKey) Equal(s OneTimeKey) bool { + if otk.ID != s.ID { + return false + } + if otk.Published != s.Published { + return false + } + if !otk.Key.PrivateKey.Equal(s.Key.PrivateKey) { + return false + } + if !otk.Key.PublicKey.Equal(s.Key.PublicKey) { + return false + } + return true +} + +// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c OneTimeKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle one time key: %w", olm.ErrValueTooShort) + } + written := libolmpickle.PickleUInt32(uint32(c.ID), target) + written += libolmpickle.PickleBool(c.Published, target[written:]) + writtenKey, err := c.Key.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle one time key: %w", err) + } + written += writtenKey + return written, nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the OneTimeKey accordingly. It returns the number of bytes read. +func (c *OneTimeKey) UnpickleLibOlm(value []byte) (int, error) { + totalReadBytes := 0 + id, readBytes, err := libolmpickle.UnpickleUInt32(value) + if err != nil { + return 0, err + } + totalReadBytes += readBytes + c.ID = id + published, readBytes, err := libolmpickle.UnpickleBool(value[totalReadBytes:]) + if err != nil { + return 0, err + } + totalReadBytes += readBytes + c.Published = published + readBytes, err = c.Key.UnpickleLibOlm(value[totalReadBytes:]) + if err != nil { + return 0, err + } + totalReadBytes += readBytes + return totalReadBytes, nil +} + +// PickleLen returns the number of bytes the pickled OneTimeKey will have. +func (c OneTimeKey) PickleLen() int { + length := 0 + length += libolmpickle.PickleUInt32Len(c.ID) + length += libolmpickle.PickleBoolLen(c.Published) + length += c.Key.PickleLen() + return length +} + +// KeyIDEncoded returns the base64 encoded id. +func (c OneTimeKey) KeyIDEncoded() string { + resSlice := make([]byte, 4) + binary.BigEndian.PutUint32(resSlice, c.ID) + return base64.RawStdEncoding.EncodeToString(resSlice) +} + +// PublicKeyEncoded returns the base64 encoded public key +func (c OneTimeKey) PublicKeyEncoded() id.Curve25519 { + return c.Key.PublicKey.B64Encoded() +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go new file mode 100644 index 0000000..ec125a3 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go @@ -0,0 +1,41 @@ +package libolmpickle + +import ( + "encoding/binary" +) + +func PickleUInt8(value uint8, target []byte) int { + target[0] = value + return 1 +} +func PickleUInt8Len(value uint8) int { + return 1 +} + +func PickleBool(value bool, target []byte) int { + if value { + target[0] = 0x01 + } else { + target[0] = 0x00 + } + return 1 +} +func PickleBoolLen(value bool) int { + return 1 +} + +func PickleBytes(value, target []byte) int { + return copy(target, value) +} +func PickleBytesLen(value []byte) int { + return len(value) +} + +func PickleUInt32(value uint32, target []byte) int { + res := make([]byte, 4) //4 bytes for int32 + binary.BigEndian.PutUint32(res, value) + return copy(target, res) +} +func PickleUInt32Len(value uint32) int { + return 4 +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go new file mode 100644 index 0000000..dbd275a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go @@ -0,0 +1,53 @@ +package libolmpickle + +import ( + "fmt" + + "maunium.net/go/mautrix/crypto/olm" +) + +func isZeroByteSlice(bytes []byte) bool { + b := byte(0) + for _, s := range bytes { + b |= s + } + return b == 0 +} + +func UnpickleUInt8(value []byte) (uint8, int, error) { + if len(value) < 1 { + return 0, 0, fmt.Errorf("unpickle uint8: %w", olm.ErrValueTooShort) + } + return value[0], 1, nil +} + +func UnpickleBool(value []byte) (bool, int, error) { + if len(value) < 1 { + return false, 0, fmt.Errorf("unpickle bool: %w", olm.ErrValueTooShort) + } + return value[0] != uint8(0x00), 1, nil +} + +func UnpickleBytes(value []byte, length int) ([]byte, int, error) { + if len(value) < length { + return nil, 0, fmt.Errorf("unpickle bytes: %w", olm.ErrValueTooShort) + } + resp := value[:length] + if isZeroByteSlice(resp) { + return nil, length, nil + } + return resp, length, nil +} + +func UnpickleUInt32(value []byte) (uint32, int, error) { + if len(value) < 4 { + return 0, 0, fmt.Errorf("unpickle uint32: %w", olm.ErrValueTooShort) + } + var res uint32 + count := 0 + for i := 3; i >= 0; i-- { + res |= uint32(value[count]) << (8 * i) + count++ + } + return res, 4, nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/README.md b/vendor/maunium.net/go/mautrix/crypto/olm/README.md new file mode 100644 index 0000000..7d8086c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/README.md @@ -0,0 +1,4 @@ +# Go olm bindings +Based on [Dhole/go-olm](https://github.com/Dhole/go-olm) + +The original project is licensed under the Apache 2.0 license. diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/account.go b/vendor/maunium.net/go/mautrix/crypto/olm/account.go new file mode 100644 index 0000000..3271b1c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/account.go @@ -0,0 +1,113 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 olm + +import ( + "io" + + "maunium.net/go/mautrix/id" +) + +type Account interface { + // Pickle returns an Account as a base64 string. Encrypts the Account using the + // supplied key. + Pickle(key []byte) ([]byte, error) + + // Unpickle loads an Account from a pickled base64 string. Decrypts the + // Account using the supplied key. Returns error on failure. + Unpickle(pickled, key []byte) error + + // IdentityKeysJSON returns the public parts of the identity keys for the Account. + IdentityKeysJSON() ([]byte, error) + + // IdentityKeys returns the public parts of the Ed25519 and Curve25519 identity + // keys for the Account. + IdentityKeys() (id.Ed25519, id.Curve25519, error) + + // Sign returns the signature of a message using the ed25519 key for this + // Account. + Sign(message []byte) ([]byte, error) + + // OneTimeKeys returns the public parts of the unpublished one time keys for + // the Account. + // + // The returned data is a struct with the single value "Curve25519", which is + // itself an object mapping key id to base64-encoded Curve25519 key. For + // example: + // + // { + // Curve25519: { + // "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo", + // "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU" + // } + // } + OneTimeKeys() (map[string]id.Curve25519, error) + + // MarkKeysAsPublished marks the current set of one time keys as being + // published. + MarkKeysAsPublished() + + // MaxNumberOfOneTimeKeys returns the largest number of one time keys this + // Account can store. + MaxNumberOfOneTimeKeys() uint + + // GenOneTimeKeys generates a number of new one time keys. If the total + // number of keys stored by this Account exceeds MaxNumberOfOneTimeKeys + // then the old keys are discarded. Reads random data from the given + // reader, or if nil is passed, defaults to crypto/rand. + GenOneTimeKeys(reader io.Reader, num uint) error + + // NewOutboundSession creates a new out-bound session for sending messages to a + // given curve25519 identityKey and oneTimeKey. Returns error on failure. If the + // keys couldn't be decoded as base64 then the error will be "INVALID_BASE64" + NewOutboundSession(theirIdentityKey, theirOneTimeKey id.Curve25519) (Session, error) + + // NewInboundSession creates a new in-bound session for sending/receiving + // messages from an incoming PRE_KEY message. Returns error on failure. If + // the base64 couldn't be decoded then the error will be "INVALID_BASE64". If + // the message was for an unsupported protocol version then the error will be + // "BAD_MESSAGE_VERSION". If the message couldn't be decoded then then the + // error will be "BAD_MESSAGE_FORMAT". If the message refers to an unknown one + // time key then the error will be "BAD_MESSAGE_KEY_ID". + NewInboundSession(oneTimeKeyMsg string) (Session, error) + + // NewInboundSessionFrom creates a new in-bound session for sending/receiving + // messages from an incoming PRE_KEY message. Returns error on failure. If + // the base64 couldn't be decoded then the error will be "INVALID_BASE64". If + // the message was for an unsupported protocol version then the error will be + // "BAD_MESSAGE_VERSION". If the message couldn't be decoded then then the + // error will be "BAD_MESSAGE_FORMAT". If the message refers to an unknown one + // time key then the error will be "BAD_MESSAGE_KEY_ID". + NewInboundSessionFrom(theirIdentityKey *id.Curve25519, oneTimeKeyMsg string) (Session, error) + + // RemoveOneTimeKeys removes the one time keys that the session used from the + // Account. Returns error on failure. If the Account doesn't have any + // matching one time keys then the error will be "BAD_MESSAGE_KEY_ID". + RemoveOneTimeKeys(s Session) error +} + +var InitBlankAccount func() Account +var InitNewAccount func(io.Reader) (Account, error) +var InitNewAccountFromPickled func(pickled, key []byte) (Account, error) + +// NewAccount creates a new Account. +func NewAccount(r io.Reader) (Account, error) { + return InitNewAccount(r) +} + +func NewBlankAccount() Account { + return InitBlankAccount() +} + +// AccountFromPickled loads an Account from a pickled base64 string. Decrypts +// the Account using the supplied key. Returns error on failure. If the key +// doesn't match the one used to encrypt the Account then the error will be +// "BAD_ACCOUNT_KEY". If the base64 couldn't be decoded then the error will be +// "INVALID_BASE64". +func AccountFromPickled(pickled, key []byte) (Account, error) { + return InitNewAccountFromPickled(pickled, key) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/errors.go b/vendor/maunium.net/go/mautrix/crypto/olm/errors.go new file mode 100644 index 0000000..c80b82e --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/errors.go @@ -0,0 +1,60 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 olm + +import "errors" + +// Those are the most common used errors +var ( + ErrBadSignature = errors.New("bad signature") + ErrBadMAC = errors.New("bad mac") + ErrBadMessageFormat = errors.New("bad message format") + ErrBadVerification = errors.New("bad verification") + ErrWrongProtocolVersion = errors.New("wrong protocol version") + ErrEmptyInput = errors.New("empty input") + ErrNoKeyProvided = errors.New("no key") + ErrBadMessageKeyID = errors.New("bad message key id") + ErrRatchetNotAvailable = errors.New("ratchet not available: attempt to decode a message whose index is earlier than our earliest known session key") + ErrMsgIndexTooHigh = errors.New("message index too high") + ErrProtocolViolation = errors.New("not protocol message order") + ErrMessageKeyNotFound = errors.New("message key not found") + ErrChainTooHigh = errors.New("chain index too high") + ErrBadInput = errors.New("bad input") + ErrBadVersion = errors.New("wrong version") + ErrWrongPickleVersion = errors.New("wrong pickle version") + ErrValueTooShort = errors.New("value too short") + ErrInputToSmall = errors.New("input too small (truncated?)") + ErrOverflow = errors.New("overflow") +) + +// Error codes from go-olm +var ( + EmptyInput = errors.New("empty input") + NoKeyProvided = errors.New("no pickle key provided") + NotEnoughGoRandom = errors.New("couldn't get enough randomness from crypto/rand") + SignatureNotFound = errors.New("input JSON doesn't contain signature from specified device") + InputNotJSONString = errors.New("input doesn't look like a JSON string") +) + +// Error codes from olm code +var ( + NotEnoughRandom = errors.New("not enough entropy was supplied") + OutputBufferTooSmall = errors.New("supplied output buffer is too small") + BadMessageVersion = errors.New("the message version is unsupported") + BadMessageFormat = errors.New("the message couldn't be decoded") + BadMessageMAC = errors.New("the message couldn't be decrypted") + BadMessageKeyID = errors.New("the message references an unknown key ID") + InvalidBase64 = errors.New("the input base64 was invalid") + BadAccountKey = errors.New("the supplied account key is invalid") + UnknownPickleVersion = errors.New("the pickled object is too new") + CorruptedPickle = errors.New("the pickled object couldn't be decoded") + BadSessionKey = errors.New("attempt to initialise an inbound group session from an invalid session key") + UnknownMessageIndex = errors.New("attempt to decode a message whose index is earlier than our earliest known session key") + BadLegacyAccountPickle = errors.New("attempt to unpickle an account which uses pickle version 1") + BadSignature = errors.New("received message had a bad signature") + InputBufferTooSmall = errors.New("the input data was too small to be valid") +) diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go b/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go new file mode 100644 index 0000000..8839b48 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go @@ -0,0 +1,80 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 olm + +import "maunium.net/go/mautrix/id" + +type InboundGroupSession interface { + // Pickle returns an InboundGroupSession as a base64 string. Encrypts the + // InboundGroupSession using the supplied key. + Pickle(key []byte) ([]byte, error) + + // Unpickle loads an [InboundGroupSession] from a pickled base64 string. + // Decrypts the [InboundGroupSession] using the supplied key. + Unpickle(pickled, key []byte) error + + // Decrypt decrypts a message using the [InboundGroupSession]. Returns the + // plain-text and message index on success. Returns error on failure. If + // the base64 couldn't be decoded then the error will be "INVALID_BASE64". + // If the message is for an unsupported version of the protocol then the + // error will be "BAD_MESSAGE_VERSION". If the message couldn't be decoded + // then the error will be BAD_MESSAGE_FORMAT". If the MAC on the message + // was invalid then the error will be "BAD_MESSAGE_MAC". If we do not have + // a session key corresponding to the message's index (ie, it was sent + // before the session key was shared with us) the error will be + // "OLM_UNKNOWN_MESSAGE_INDEX". + Decrypt(message []byte) ([]byte, uint, error) + + // ID returns a base64-encoded identifier for this session. + ID() id.SessionID + + // FirstKnownIndex returns the first message index we know how to decrypt. + FirstKnownIndex() uint32 + + // IsVerified check if the session has been verified as a valid session. + // (A session is verified either because the original session share was + // signed, or because we have subsequently successfully decrypted a + // message.) + IsVerified() bool + + // Export returns the base64-encoded ratchet key for this session, at the + // given index, in a format which can be used by + // InboundGroupSession.InboundGroupSessionImport(). Encrypts the + // InboundGroupSession using the supplied key. Returns error on failure. + // if we do not have a session key corresponding to the given index (ie, it + // was sent before the session key was shared with us) the error will be + // "OLM_UNKNOWN_MESSAGE_INDEX". + Export(messageIndex uint32) ([]byte, error) +} + +var InitInboundGroupSessionFromPickled func(pickled, key []byte) (InboundGroupSession, error) +var InitNewInboundGroupSession func(sessionKey []byte) (InboundGroupSession, error) +var InitInboundGroupSessionImport func(sessionKey []byte) (InboundGroupSession, error) +var InitBlankInboundGroupSession func() InboundGroupSession + +// InboundGroupSessionFromPickled loads an InboundGroupSession from a pickled +// base64 string. Decrypts the InboundGroupSession using the supplied key. +// Returns error on failure. +func InboundGroupSessionFromPickled(pickled, key []byte) (InboundGroupSession, error) { + return InitInboundGroupSessionFromPickled(pickled, key) +} + +// NewInboundGroupSession creates a new inbound group session from a key +// exported from OutboundGroupSession.Key(). Returns error on failure. +func NewInboundGroupSession(sessionKey []byte) (InboundGroupSession, error) { + return InitNewInboundGroupSession(sessionKey) +} + +// InboundGroupSessionImport imports an inbound group session from a previous +// export. Returns error on failure. +func InboundGroupSessionImport(sessionKey []byte) (InboundGroupSession, error) { + return InitInboundGroupSessionImport(sessionKey) +} + +func NewBlankInboundGroupSession() InboundGroupSession { + return InitBlankInboundGroupSession() +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/olm.go b/vendor/maunium.net/go/mautrix/crypto/olm/olm.go new file mode 100644 index 0000000..fa2345e --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/olm.go @@ -0,0 +1,20 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 olm + +var GetVersion func() (major, minor, patch uint8) +var SetPickleKeyImpl func(key []byte) + +// Version returns the version number of the olm library. +func Version() (major, minor, patch uint8) { + return GetVersion() +} + +// SetPickleKey sets the global pickle key used when encoding structs with Gob or JSON. +func SetPickleKey(key []byte) { + SetPickleKeyImpl(key) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go b/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go new file mode 100644 index 0000000..c5b7bcb --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go @@ -0,0 +1,57 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 olm + +import "maunium.net/go/mautrix/id" + +type OutboundGroupSession interface { + // Pickle returns a Session as a base64 string. Encrypts the Session using + // the supplied key. + Pickle(key []byte) ([]byte, error) + + // Unpickle loads an [OutboundGroupSession] from a pickled base64 string. + // Decrypts the [OutboundGroupSession] using the supplied key. + Unpickle(pickled, key []byte) error + + // Encrypt encrypts a message using the [OutboundGroupSession]. Returns the + // encrypted message as base64. + Encrypt(plaintext []byte) ([]byte, error) + + // ID returns a base64-encoded identifier for this session. + ID() id.SessionID + + // MessageIndex returns the message index for this session. Each message + // is sent with an increasing index; this returns the index for the next + // message. + MessageIndex() uint + + // Key returns the base64-encoded current ratchet key for this session. + Key() string +} + +var InitNewOutboundGroupSessionFromPickled func(pickled, key []byte) (OutboundGroupSession, error) +var InitNewOutboundGroupSession func() OutboundGroupSession +var InitNewBlankOutboundGroupSession func() OutboundGroupSession + +// OutboundGroupSessionFromPickled loads an OutboundGroupSession from a pickled +// base64 string. Decrypts the OutboundGroupSession using the supplied key. +// Returns error on failure. If the key doesn't match the one used to encrypt +// the OutboundGroupSession then the error will be "BAD_SESSION_KEY". If the +// base64 couldn't be decoded then the error will be "INVALID_BASE64". +func OutboundGroupSessionFromPickled(pickled, key []byte) (OutboundGroupSession, error) { + return InitNewOutboundGroupSessionFromPickled(pickled, key) +} + +// NewOutboundGroupSession creates a new outbound group session. +func NewOutboundGroupSession() OutboundGroupSession { + return InitNewOutboundGroupSession() +} + +// NewBlankOutboundGroupSession initialises an empty [OutboundGroupSession]. +func NewBlankOutboundGroupSession() OutboundGroupSession { + return InitNewBlankOutboundGroupSession() +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/pk.go b/vendor/maunium.net/go/mautrix/crypto/olm/pk.go new file mode 100644 index 0000000..70ee452 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/pk.go @@ -0,0 +1,57 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 olm + +import ( + "maunium.net/go/mautrix/id" +) + +// PKSigning is an interface for signing messages. +type PKSigning interface { + // Seed returns the seed of the key. + Seed() []byte + + // PublicKey returns the public key. + PublicKey() id.Ed25519 + + // Sign creates a signature for the given message using this key. + Sign(message []byte) ([]byte, error) + + // SignJSON creates a signature for the given object after encoding it to + // canonical JSON. + SignJSON(obj any) (string, error) +} + +// PKDecryption is an interface for decrypting messages. +type PKDecryption interface { + // PublicKey returns the public key. + PublicKey() id.Curve25519 + + // Decrypt verifies and decrypts the given message. + Decrypt(ephemeralKey, mac, ciphertext []byte) ([]byte, error) +} + +var InitNewPKSigning func() (PKSigning, error) +var InitNewPKSigningFromSeed func(seed []byte) (PKSigning, error) +var InitNewPKDecryptionFromPrivateKey func(privateKey []byte) (PKDecryption, error) + +// NewPKSigning creates a new [PKSigning] object, containing a key pair for +// signing messages. +func NewPKSigning() (PKSigning, error) { + return InitNewPKSigning() +} + +// NewPKSigningFromSeed creates a new PKSigning object using the given seed. +func NewPKSigningFromSeed(seed []byte) (PKSigning, error) { + return InitNewPKSigningFromSeed(seed) +} + +// NewPKDecryptionFromPrivateKey creates a new [PKDecryption] from a +// base64-encoded private key. +func NewPKDecryptionFromPrivateKey(privateKey []byte) (PKDecryption, error) { + return InitNewPKDecryptionFromPrivateKey(privateKey) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/session.go b/vendor/maunium.net/go/mautrix/crypto/olm/session.go new file mode 100644 index 0000000..c4b91ff --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/session.go @@ -0,0 +1,83 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 olm + +import "maunium.net/go/mautrix/id" + +type Session interface { + // Pickle returns a Session as a base64 string. Encrypts the Session using + // the supplied key. + Pickle(key []byte) ([]byte, error) + + // Unpickle loads a Session from a pickled base64 string. Decrypts the + // Session using the supplied key. + Unpickle(pickled, key []byte) error + + // ID returns an identifier for this Session. Will be the same for both + // ends of the conversation. + ID() id.SessionID + + // HasReceivedMessage returns true if this session has received any + // message. + HasReceivedMessage() bool + + // MatchesInboundSession checks if the PRE_KEY message is for this in-bound + // Session. This can happen if multiple messages are sent to this Account + // before this Account sends a message in reply. Returns true if the + // session matches. Returns false if the session does not match. Returns + // error on failure. If the base64 couldn't be decoded then the error will + // be "INVALID_BASE64". If the message was for an unsupported protocol + // version then the error will be "BAD_MESSAGE_VERSION". If the message + // couldn't be decoded then then the error will be "BAD_MESSAGE_FORMAT". + MatchesInboundSession(oneTimeKeyMsg string) (bool, error) + + // MatchesInboundSessionFrom checks if the PRE_KEY message is for this + // in-bound Session. This can happen if multiple messages are sent to this + // Account before this Account sends a message in reply. Returns true if + // the session matches. Returns false if the session does not match. + // Returns error on failure. If the base64 couldn't be decoded then the + // error will be "INVALID_BASE64". If the message was for an unsupported + // protocol version then the error will be "BAD_MESSAGE_VERSION". If the + // message couldn't be decoded then then the error will be + // "BAD_MESSAGE_FORMAT". + MatchesInboundSessionFrom(theirIdentityKey, oneTimeKeyMsg string) (bool, error) + + // EncryptMsgType returns the type of the next message that Encrypt will + // return. Returns MsgTypePreKey if the message will be a PRE_KEY message. + // Returns MsgTypeMsg if the message will be a normal message. + EncryptMsgType() id.OlmMsgType + + // Encrypt encrypts a message using the Session. Returns the encrypted + // message as base64. + Encrypt(plaintext []byte) (id.OlmMsgType, []byte, error) + + // Decrypt decrypts a message using the Session. Returns the plain-text on + // success. Returns error on failure. If the base64 couldn't be decoded + // then the error will be "INVALID_BASE64". If the message is for an + // unsupported version of the protocol then the error will be + // "BAD_MESSAGE_VERSION". If the message couldn't be decoded then the error + // will be BAD_MESSAGE_FORMAT". If the MAC on the message was invalid then + // the error will be "BAD_MESSAGE_MAC". + Decrypt(message string, msgType id.OlmMsgType) ([]byte, error) + + // Describe generates a string describing the internal state of an olm + // session for debugging and logging purposes. + Describe() string +} + +var InitSessionFromPickled func(pickled, key []byte) (Session, error) +var InitNewBlankSession func() Session + +// SessionFromPickled loads a Session from a pickled base64 string. Decrypts +// the Session using the supplied key. Returns error on failure. +func SessionFromPickled(pickled, key []byte) (Session, error) { + return InitSessionFromPickled(pickled, key) +} + +func NewBlankSession() Session { + return InitNewBlankSession() +} diff --git a/vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go b/vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go new file mode 100644 index 0000000..dc28ed6 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go @@ -0,0 +1,30 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 pkcs7 + +import "bytes" + +// Pad implements PKCS#7 padding as defined in [RFC2315]. It pads the data to +// the given blockSize in the range [1, 255]. This is normally used in AES-CBC +// encryption. +// +// [RFC2315]: https://www.ietf.org/rfc/rfc2315.txt +func Pad(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + return append(data, bytes.Repeat([]byte{byte(padding)}, padding)...) +} + +// Unpad implements PKCS#7 unpadding as defined in [RFC2315]. It unpads the +// data by reading the padding amount from the last byte of the data. This is +// normally used in AES-CBC decryption. +// +// [RFC2315]: https://www.ietf.org/rfc/rfc2315.txt +func Unpad(data []byte) []byte { + length := len(data) + unpadding := int(data[length-1]) + return data[:length-unpadding] +} diff --git a/vendor/maunium.net/go/mautrix/crypto/signatures/signatures.go b/vendor/maunium.net/go/mautrix/crypto/signatures/signatures.go new file mode 100644 index 0000000..0c4422f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/signatures/signatures.go @@ -0,0 +1,94 @@ +// Copyright (c) 2024 Sumner Evans +// +// 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 signatures + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + "go.mau.fi/util/exgjson" + + "maunium.net/go/mautrix/crypto/canonicaljson" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/id" +) + +var ( + ErrEmptyInput = errors.New("empty input") + ErrSignatureNotFound = errors.New("input JSON doesn't contain signature from specified device") +) + +// Signatures represents a set of signatures for some data from multiple users +// and keys. +type Signatures map[id.UserID]map[id.KeyID]string + +// NewSingleSignature creates a new [Signatures] object with a single +// signature. +func NewSingleSignature(userID id.UserID, algorithm id.KeyAlgorithm, keyID string, signature string) Signatures { + return Signatures{ + userID: { + id.NewKeyID(algorithm, keyID): signature, + }, + } +} + +// VerifySignature verifies an Ed25519 signature. +func VerifySignature(message []byte, key id.Ed25519, signature []byte) (ok bool, err error) { + if len(message) == 0 || len(key) == 0 || len(signature) == 0 { + return false, ErrEmptyInput + } + keyDecoded, err := base64.RawStdEncoding.DecodeString(key.String()) + if err != nil { + return false, err + } + publicKey := crypto.Ed25519PublicKey(keyDecoded) + return publicKey.Verify(message, signature), nil +} + +// VerifySignatureJSON verifies the signature in the given JSON object "obj" +// as described in [Appendix 3] of the Matrix Spec. +// +// This function is a wrapper over [Utility.VerifySignatureJSON] that creates +// and destroys the [Utility] object transparently. +// +// If the "obj" is not already a [json.RawMessage], it will re-encoded as JSON +// for the verification, so "json" tags will be honored. +// +// [Appendix 3]: https://spec.matrix.org/v1.9/appendices/#signing-json +func VerifySignatureJSON(obj any, userID id.UserID, keyName string, key id.Ed25519) (bool, error) { + var err error + objJSON, ok := obj.(json.RawMessage) + if !ok { + objJSON, err = json.Marshal(obj) + if err != nil { + return false, err + } + } + + sig := gjson.GetBytes(objJSON, exgjson.Path("signatures", string(userID), fmt.Sprintf("ed25519:%s", keyName))) + if !sig.Exists() || sig.Type != gjson.String { + return false, ErrSignatureNotFound + } + objJSON, err = sjson.DeleteBytes(objJSON, "unsigned") + if err != nil { + return false, err + } + objJSON, err = sjson.DeleteBytes(objJSON, "signatures") + if err != nil { + return false, err + } + objJSONString := canonicaljson.CanonicalJSONAssumeValid(objJSON) + sigBytes, err := base64.RawStdEncoding.DecodeString(sig.Str) + if err != nil { + return false, err + } + return VerifySignature(objJSONString, key, sigBytes) +} 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 0000000..e2f8a19 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go @@ -0,0 +1,132 @@ +// 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/rand" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "strings" + + "go.mau.fi/util/base58" + "golang.org/x/crypto/hkdf" + "golang.org/x/crypto/pbkdf2" +) + +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 unpadded 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.RawStdEncoding.EncodeToString(h.Sum(nil)) +} |