2018-03-09 20:44:13 +00:00
package model
import (
2021-05-14 18:26:04 +00:00
"crypto/ed25519"
2018-03-09 20:44:13 +00:00
"crypto/rand"
2021-05-14 18:26:04 +00:00
"crypto/sha512"
2020-07-14 00:46:05 +00:00
"cwtch.im/cwtch/protocol/groups"
2021-05-14 18:26:04 +00:00
"encoding/base32"
2021-05-03 23:32:48 +00:00
"encoding/base64"
2021-05-14 18:26:04 +00:00
"encoding/hex"
2020-07-14 00:46:05 +00:00
"encoding/json"
2018-05-16 20:20:46 +00:00
"errors"
2018-03-09 20:44:13 +00:00
"fmt"
2021-11-19 20:27:52 +00:00
"git.openprivacy.ca/cwtch.im/tapir/primitives"
2020-02-10 22:09:24 +00:00
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
2018-03-30 21:16:51 +00:00
"golang.org/x/crypto/nacl/secretbox"
2021-05-14 18:26:04 +00:00
"golang.org/x/crypto/pbkdf2"
2018-03-30 21:16:51 +00:00
"io"
2021-05-14 18:26:04 +00:00
"strings"
2021-11-19 20:27:52 +00:00
"time"
2018-03-09 20:44:13 +00:00
)
2020-09-28 18:18:18 +00:00
// CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date
2021-08-25 19:16:50 +00:00
const CurrentGroupVersion = 4
2021-05-14 18:26:04 +00:00
// GroupInvitePrefix identifies a particular string as being a serialized group invite.
const GroupInvitePrefix = "torv3"
2020-09-28 18:18:18 +00:00
2018-10-05 03:18:34 +00:00
// Group defines and encapsulates Cwtch's conception of group chat. Which are sessions
2019-02-03 01:18:33 +00:00
// tied to a server under a given group key. Each group has a set of Messages.
2018-03-09 20:44:13 +00:00
type Group struct {
2021-05-14 18:26:04 +00:00
// GroupID is now derived from the GroupKey and the GroupServer
2021-11-11 00:41:43 +00:00
GroupID string
2021-11-17 22:34:13 +00:00
GroupName string
2021-11-11 00:41:43 +00:00
GroupKey [ 32 ] byte
GroupServer string
2021-11-18 23:43:58 +00:00
Attributes map [ string ] string //legacy to not use
2021-11-11 00:41:43 +00:00
Version int
Timeline Timeline ` json:"-" `
LocalID string
2018-03-09 20:44:13 +00:00
}
2018-03-15 16:33:26 +00:00
// NewGroup initializes a new group associated with a given CwtchServer
2018-09-27 00:08:54 +00:00
func NewGroup ( server string ) ( * Group , error ) {
2018-03-09 20:44:13 +00:00
group := new ( Group )
2021-06-02 18:34:57 +00:00
if ! tor . IsValidHostname ( server ) {
return nil , errors . New ( "server is not a valid v3 onion" )
2018-11-21 22:15:43 +00:00
}
2018-03-09 20:44:13 +00:00
group . GroupServer = server
var groupKey [ 32 ] byte
if _ , err := io . ReadFull ( rand . Reader , groupKey [ : ] ) ; err != nil {
2018-12-04 02:52:11 +00:00
log . Errorf ( "Error: Cannot read from random: %v\n" , err )
2018-09-27 00:08:54 +00:00
return nil , err
2018-03-09 20:44:13 +00:00
}
copy ( group . GroupKey [ : ] , groupKey [ : ] )
2021-05-14 18:26:04 +00:00
// Derive Group ID from the group key and the server public key. This binds the group to a particular server
// and key.
group . GroupID = deriveGroupID ( groupKey [ : ] , server )
2018-09-27 00:08:54 +00:00
return group , nil
2018-03-09 20:44:13 +00:00
}
2021-05-18 19:09:11 +00:00
// CheckGroup returns true only if the ID of the group is cryptographically valid.
func ( g * Group ) CheckGroup ( ) bool {
return g . GroupID == deriveGroupID ( g . GroupKey [ : ] , g . GroupServer )
}
2021-05-18 19:11:00 +00:00
// deriveGroupID hashes together the key and the hostname to create a bound identifier that can later
// be referenced and checked by profiles when they receive invites and messages.
2021-05-14 18:26:04 +00:00
func deriveGroupID ( groupKey [ ] byte , serverHostname string ) string {
data , _ := base32 . StdEncoding . DecodeString ( strings . ToUpper ( serverHostname ) )
pubkey := data [ 0 : ed25519 . PublicKeySize ]
2021-05-18 19:09:11 +00:00
return hex . EncodeToString ( pbkdf2 . Key ( groupKey , pubkey , 4096 , 16 , sha512 . New ) )
2018-03-15 20:53:22 +00:00
}
2018-05-16 20:18:47 +00:00
// Invite generates a invitation that can be sent to a cwtch peer
2021-11-17 22:34:13 +00:00
func ( g * Group ) Invite ( ) ( string , error ) {
2018-05-16 20:18:47 +00:00
2020-07-14 00:46:05 +00:00
gci := & groups . GroupInvite {
2021-05-14 18:26:04 +00:00
GroupID : g . GroupID ,
2021-11-17 22:34:13 +00:00
GroupName : g . GroupName ,
2021-05-14 18:26:04 +00:00
SharedKey : g . GroupKey [ : ] ,
ServerHost : g . GroupServer ,
2018-03-15 20:53:22 +00:00
}
2018-04-28 17:52:59 +00:00
2020-07-14 00:46:05 +00:00
invite , err := json . Marshal ( gci )
2021-05-14 18:26:04 +00:00
serializedInvite := fmt . Sprintf ( "%v%v" , GroupInvitePrefix , base64 . StdEncoding . EncodeToString ( invite ) )
2021-05-03 23:32:48 +00:00
return serializedInvite , err
2018-03-15 20:53:22 +00:00
}
2018-03-15 16:33:26 +00:00
//EncryptMessage takes a message and encrypts the message under the group key.
2020-07-14 00:46:05 +00:00
func ( g * Group ) EncryptMessage ( message * groups . DecryptedGroupMessage ) ( [ ] byte , error ) {
2018-03-09 20:44:13 +00:00
var nonce [ 24 ] byte
if _ , err := io . ReadFull ( rand . Reader , nonce [ : ] ) ; err != nil {
2018-12-04 02:52:11 +00:00
log . Errorf ( "Cannot read from random: %v\n" , err )
2018-09-27 00:08:54 +00:00
return nil , err
2018-03-09 20:44:13 +00:00
}
2020-07-14 00:46:05 +00:00
wire , err := json . Marshal ( message )
2019-11-08 00:39:27 +00:00
if err != nil {
return nil , err
}
2018-03-15 20:53:22 +00:00
encrypted := secretbox . Seal ( nonce [ : ] , [ ] byte ( wire ) , & nonce , & g . GroupKey )
2018-09-27 00:08:54 +00:00
return encrypted , nil
2018-03-09 20:44:13 +00:00
}
2018-03-15 16:33:26 +00:00
// DecryptMessage takes a ciphertext and returns true and the decrypted message if the
// cipher text can be successfully decrypted,else false.
2020-07-14 00:46:05 +00:00
func ( g * Group ) DecryptMessage ( ciphertext [ ] byte ) ( bool , * groups . DecryptedGroupMessage ) {
2019-11-08 00:39:27 +00:00
if len ( ciphertext ) > 24 {
var decryptNonce [ 24 ] byte
copy ( decryptNonce [ : ] , ciphertext [ : 24 ] )
decrypted , ok := secretbox . Open ( nil , ciphertext [ 24 : ] , & decryptNonce , & g . GroupKey )
if ok {
2020-07-14 00:46:05 +00:00
dm := & groups . DecryptedGroupMessage { }
err := json . Unmarshal ( decrypted , dm )
2019-11-08 00:39:27 +00:00
if err == nil {
return true , dm
}
2018-03-15 20:53:22 +00:00
}
2018-03-09 20:44:13 +00:00
}
2018-03-15 20:53:22 +00:00
return false , nil
2018-03-09 20:44:13 +00:00
}
2018-11-02 23:43:40 +00:00
2021-05-14 18:26:04 +00:00
// ValidateInvite takes in a serialized invite and returns the invite structure if it is cryptographically valid
// and an error if it is not
func ValidateInvite ( invite string ) ( * groups . GroupInvite , error ) {
// We prefix invites for groups with torv3
if strings . HasPrefix ( invite , GroupInvitePrefix ) {
2021-05-18 19:23:13 +00:00
data , err := base64 . StdEncoding . DecodeString ( invite [ len ( GroupInvitePrefix ) : ] )
2021-05-14 18:26:04 +00:00
if err == nil {
// First attempt to unmarshal the json...
var gci groups . GroupInvite
err := json . Unmarshal ( data , & gci )
if err == nil {
// Validate the Invite by first checking that the server is a valid v3 onion
2021-06-02 18:34:57 +00:00
if ! tor . IsValidHostname ( gci . ServerHost ) {
2021-05-14 18:26:04 +00:00
return nil , errors . New ( "server is not a valid v3 onion" )
}
// Validate the length of the shared key...
if len ( gci . SharedKey ) != 32 {
return nil , errors . New ( "key length is not 32 bytes" )
}
// Derive the servers public key (we can ignore the error checking here because it's already been
// done by IsValidHostname, and check that we derive the same groupID...
derivedGroupID := deriveGroupID ( gci . SharedKey , gci . ServerHost )
if derivedGroupID != gci . GroupID {
return nil , errors . New ( "group id is invalid" )
}
// Replace the original with the derived, this should be a no-op at this point but defense in depth...
gci . GroupID = derivedGroupID
return & gci , nil
}
}
}
return nil , errors . New ( "invite has invalid structure" )
}
2021-11-16 23:06:30 +00:00
// AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups.
// If successful, adds the message to the group's timeline
func ( g * Group ) AttemptDecryption ( ciphertext [ ] byte , signature [ ] byte ) ( bool , * groups . DecryptedGroupMessage ) {
success , dgm := g . DecryptMessage ( ciphertext )
if success {
// Attempt to serialize this message
serialized , err := json . Marshal ( dgm )
// Someone send a message that isn't a valid Decrypted Group Message. Since we require this struct in orer
// to verify the message, we simply ignore it.
if err != nil {
return false , nil
}
// This now requires knowledge of the Sender, the Onion and the Specific Decrypted Group Message (which should only
// be derivable from the cryptographic key) which contains many unique elements such as the time and random padding
verified := g . VerifyGroupMessage ( dgm . Onion , g . GroupID , base64 . StdEncoding . EncodeToString ( serialized ) , signature )
if ! verified {
// An earlier version of this protocol mistakenly signed the ciphertext of the message
// instead of the serialized decrypted group message.
// This has 2 issues:
// 1. A server with knowledge of group members public keys AND the Group ID would be able to detect valid messages
// 2. It made the metadata-security of a group dependent on keeping the cryptographically derived Group ID secret.
// While not awful, it also isn't good. For Version 3 groups only we permit Cwtch to check this older signature
// structure in a backwards compatible way for the duration of the Groups Experiment.
// TODO: Delete this check when Groups are no long Experimental
if g . Version == 3 {
verified = g . VerifyGroupMessage ( dgm . Onion , g . GroupID , string ( ciphertext ) , signature )
}
}
// So we have a message that has a valid group key, but the signature can't be verified.
// The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious)
// Either way, someone who has the private key is being detectably bad so we are just going to throw this message away and mark the group as Compromised.
if ! verified {
return false , nil
}
return true , dgm
}
// If we couldn't find a group to decrypt the message with we just return false. This is an expected case
return false , nil
}
// VerifyGroupMessage confirms the authenticity of a message given an sender onion, message and signature.
// The goal of this function is 2-fold:
// 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least
// knows the senders private key)
// 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't
// matter if we actually received this message from the server or from a hybrid protocol, all that matters is
// that the sender and receivers agree that this message was intended for the group
// The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at
// https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups
// on two different servers with the same key and then forwards messages between them to convince the parties in
// each group that they are actually in one big group (with the intent to later censor and/or selectively send messages
// to each group).
func ( g * Group ) VerifyGroupMessage ( onion string , groupID string , message string , signature [ ] byte ) bool {
// We use our group id, a known reference server and the ciphertext of the message.
m := groupID + g . GroupServer + message
// Otherwise we derive the public key from the sender and check it against that.
decodedPub , err := base32 . StdEncoding . DecodeString ( strings . ToUpper ( onion ) )
if err == nil && len ( decodedPub ) >= 32 {
return ed25519 . Verify ( decodedPub [ : 32 ] , [ ] byte ( m ) , signature )
}
return false
}
2021-11-19 20:27:52 +00:00
// EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
// profile
2021-12-06 20:20:38 +00:00
func EncryptMessageToGroup ( message string , author primitives . Identity , group * Group , prevSig string ) ( [ ] byte , [ ] byte , * groups . DecryptedGroupMessage , error ) {
2021-11-19 20:27:52 +00:00
if len ( message ) > MaxGroupMessageLength {
return nil , nil , nil , errors . New ( "group message is too long" )
}
timestamp := time . Now ( ) . Unix ( )
lenPadding := MaxGroupMessageLength - len ( message )
padding := make ( [ ] byte , lenPadding )
getRandomness ( & padding )
hexGroupID , err := hex . DecodeString ( group . GroupID )
if err != nil {
return nil , nil , nil , err
}
2021-12-06 20:20:38 +00:00
prevSigBytes , err := base64 . StdEncoding . DecodeString ( prevSig )
if err != nil {
return nil , nil , nil , err
}
2021-11-19 20:27:52 +00:00
dm := & groups . DecryptedGroupMessage {
Onion : author . Hostname ( ) ,
Text : message ,
SignedGroupID : hexGroupID ,
Timestamp : uint64 ( timestamp ) ,
2021-12-06 20:20:38 +00:00
PreviousMessageSig : prevSigBytes ,
2021-11-19 20:27:52 +00:00
Padding : padding [ : ] ,
}
ciphertext , err := group . EncryptMessage ( dm )
if err != nil {
return nil , nil , nil , err
}
serialized , _ := json . Marshal ( dm )
signature := author . Sign ( [ ] byte ( group . GroupID + group . GroupServer + base64 . StdEncoding . EncodeToString ( serialized ) ) )
return ciphertext , signature , dm , nil
}