diff --git a/config.go b/config.go index ecf6840..6814a88 100644 --- a/config.go +++ b/config.go @@ -26,6 +26,8 @@ type Config struct { AlwaysTrustPeerID bool `yaml:"alwaysTrustPeerID"` // Workaround until proper handling of peer reregistering with new ID. AccountCapabilities AccountCapabilities `yaml:"accountCapabilities"` // Account Attrributes are used in order to track the support of different function for signal DiscoverableByPhoneNumber bool `yaml:"discoverableByPhoneNumber"` // If the user should be found by his phone number + ProfileKey []byte `yaml:"profileKey"` // The profile key is used in many places to encrypt the avatar, name etc and also in groupsv2 context + Name string `yaml:"name"` } // TODO: some race conditions to be solved diff --git a/crypto/aesgcm.go b/crypto/aesgcm.go index b59e2a7..5891af8 100644 --- a/crypto/aesgcm.go +++ b/crypto/aesgcm.go @@ -7,6 +7,7 @@ import ( const TAG_LENGTH_BYTES = 16 +// AesgcmDecrypt ... func AesgcmDecrypt(key, nonce, data, mac []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { @@ -21,3 +22,18 @@ func AesgcmDecrypt(key, nonce, data, mac []byte) ([]byte, error) { return aesgcm.Open(nil, nonce, ciphertext, nil) } + +//indirect tested by profile_cipher_test.go + +//AesgcmEncrypt ... +func AesgcmEncrypt(key, nonce, input []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + return aesgcm.Seal(nil, nonce, input, nil), nil +} diff --git a/go.mod b/go.mod index 2526ff4..a854eec 100644 --- a/go.mod +++ b/go.mod @@ -22,11 +22,12 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/opennota/check v0.0.0-20180911053232-0c771f5545ff // indirect github.com/pieterbork/ed25519 v0.0.0-20200301051623-f19b832d0d2e // indirect + github.com/satori/go.uuid v1.2.0 github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989 // indirect github.com/signal-golang/ed25519 v0.0.0-20200301051623-f19b832d0d2e github.com/signal-golang/mimemagic v0.0.0-20200821045537-3f613cf2cd3f github.com/sirupsen/logrus v1.6.0 - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.6.1 github.com/stripe/safesql v0.2.0 // indirect github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 // indirect github.com/walle/lll v1.0.1 // indirect diff --git a/profiles/profile.go b/profiles/profile.go new file mode 100644 index 0000000..47d80d4 --- /dev/null +++ b/profiles/profile.go @@ -0,0 +1,116 @@ +package profiles + +import ( + "encoding/json" + "errors" + "fmt" + "math/rand" + + zkgroup "github.com/nanu-c/zkgroup" + uuidUtil "github.com/satori/go.uuid" + "github.com/signal-golang/textsecure/crypto" + "github.com/signal-golang/textsecure/transport" +) + +const ( + PROFILE_PATH = "/v1/profile/%s" + NAME_PADDED_LENGTH = 53 +) + +// Profile ... +type Profile struct { + Version string `json:"version"` + Name []byte `json:"name"` + Avatar bool `json:"avatar"` + Commitment []byte `json:"commitment"` +} + +// GenerateProfileKey generates a new ProfileKey +func GenerateProfileKey() []byte { + profileKey := make([]byte, 32) + rand.Read(profileKey) + return profileKey +} +func uuidToByte(id string) []byte { + s, _ := uuidUtil.FromString(id) + return s.Bytes() +} + +// UpdateProfile ... +func UpdateProfile(profileKey []byte, uuid, name string) error { + uuidByte := uuidToByte(uuid) + + profile := Profile{} + version, err := zkgroup.ProfileKeyGetProfileKeyVersion(profileKey, uuidByte) + if err != nil { + return err + } + commitment, err := zkgroup.ProfileKeyGetCommitment(profileKey, uuidByte) + if err != nil { + return err + } + nameCiphertext, err := encryptName(profileKey, []byte(name), NAME_PADDED_LENGTH) + if err != nil { + return err + } + profile.Version = string(version[:]) + profile.Commitment = commitment + profile.Name = nameCiphertext + profile.Avatar = false + writeProfile(profile) + if err != nil { + return err + } + return nil +} + +// WriteProfile ... +func writeProfile(profile Profile) error { + // String response = makeServiceRequest(String.format(PROFILE_PATH, ""), "PUT", requestBody); + body, err := json.Marshal(profile) + if err != nil { + return err + } + transport.Transport.PutJSON(fmt.Sprintf(PROFILE_PATH, ""), body) + return nil +} + +func encryptName(key, input []byte, paddedLength int) ([]byte, error) { + inputLength := len(input) + if inputLength > paddedLength { + return nil, errors.New("Input too long") + } + padded := append(input, make([]byte, paddedLength-inputLength)...) + nonce := make([]byte, 12) + rand.Read(nonce) + ciphertext, err := crypto.AesgcmEncrypt(key, nonce, padded) + if err != nil { + return nil, err + } + return append(nonce, ciphertext...), nil +} + +func decryptName(key, nonceAndCiphertext []byte) ([]byte, error) { + if len(nonceAndCiphertext) < 12+16+1 { + return nil, errors.New("nonceAndCipher too short") + } + nonce := nonceAndCiphertext[:12] + ciphertext := nonceAndCiphertext[12:] + padded, err := crypto.AesgcmDecrypt(key, nonce, ciphertext, []byte{}) + if err != nil { + return nil, err + } + paddedLength := len(padded) + plaintextLength := 0 + for i := paddedLength - 1; i >= 0; i-- { + if padded[i] != byte(0) { + plaintextLength = i + 1 + break + } + } + return padded[:plaintextLength], nil +} + +func getCommitment(profileKey, uuid []byte) ([]byte, error) { + return zkgroup.ProfileKeyGetCommitment(profileKey, uuid) +} diff --git a/profiles/profile_test.go b/profiles/profile_test.go new file mode 100644 index 0000000..ce291e2 --- /dev/null +++ b/profiles/profile_test.go @@ -0,0 +1,41 @@ +package textsecure +import ( + "testing" + "github.com/stretchr/testify/assert" + "math/rand" +) +func TestEncryptDecrypt(t *testing.T) { + key := make([]byte, 32) + rand.Read(key) + + name, err := encryptName(key,[]byte("Clement\ruval"), NAME_PADDED_LENGTH); + assert.Nil(t, err) + plaintext, err := decryptName(key, name); + assert.Nil(t, err) + + assert.Equal(t, []byte("Clement\ruval"), plaintext); +} + +func TestEmpty(t *testing.T) { + key := make([]byte, 32) + rand.Read(key) + + name, err := encryptName(key, []byte(""), 26); + assert.Nil(t, err) + plaintext, err := decryptName(key, name); + assert.Nil(t, err) + + assert.Equal(t, 0, len(plaintext)); +} + +var EXPECTED_RESULT = []byte{0x5a, 0x72, 0x3a, 0xce, 0xe5, 0x2c, 0x5e, 0xa0, + 0x2b, 0x92, 0xa3, 0xa3, 0x60, 0xc0, 0x95, 0x95} +var KEY = []byte{0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02} + +func TestKeyDerivation(t *testing.T) { + result, err := deriveAccessKeyFrom(KEY); + assert.Nil(t, err) + assert.Equal(t, EXPECTED_RESULT, result); +} diff --git a/server.go b/server.go index 0ce46c7..6481fc8 100644 --- a/server.go +++ b/server.go @@ -34,6 +34,7 @@ var ( SIGNAL_CDN_URL = "https://cdn.signal.org" SIGNAL_CDN2_URL = "https://cdn2.signal.org" DIRECTORY_URL = "https://api.directory.signal.org" + STORAGE_URL = "https://storage.signal.org" createAccountPath = "/v1/accounts/%s/code/%s?client=%s" // CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/code/%s?client=%s"; @@ -100,6 +101,9 @@ var ( GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s" GROUPSV2_TOKEN = "/v1/groups/token" + ATTESTATION_REQUEST = "/v1/attestation/%s" + DISCOVERY_REQUEST = "/v1/discovery/%s" + SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp" CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15" @@ -135,15 +139,15 @@ var registrationInfo RegistrationInfo // Registration const ( - responseNeedCaptcha int = 402; - responseRateLimit int = 413 + responseNeedCaptcha int = 402 + responseRateLimit int = 413 ) -func requestCode(tel, method, captcha string) (string,*int,error) { +func requestCode(tel, method, captcha string) (string, *int, error) { log.Infoln("[textsecure] request verification code for ", tel) path := fmt.Sprintf(createAccountPath, method, tel, "android") - if captcha != ""{ - path +="&captcha="+captcha + if captcha != "" { + path += "&captcha=" + captcha } resp, err := transport.Transport.Get(path) if err != nil { @@ -175,7 +179,7 @@ func requestCode(tel, method, captcha string) (string,*int,error) { } } else { defer resp.Body.Close() - return "",nil, nil + return "", nil, nil } // unofficial dev method, useful for development, with no telephony account needed on the server // if method == "dev" { @@ -329,6 +333,7 @@ type whoAmIResponse struct { UUID string `json:"uuid"` } +// GetMyUUID returns the uid from the current user func GetMyUUID() (string, error) { log.Debugln("[textsecure] get my uuid") resp, err := transport.Transport.Get(WHO_AM_I) @@ -407,6 +412,7 @@ func addNewDevice(ephemeralId, publicKey, verificationCode string) error { IdentityKeyPublic: identityKey.PublicKey.Serialize(), IdentityKeyPrivate: identityKey.PrivateKey.Key()[:], Number: &config.Tel, + Uuid: &config.UUID, ProvisioningCode: &verificationCode, } @@ -766,7 +772,7 @@ func createMessage(msg *outgoingMessage) *signalservice.DataMessage { if msg.groupV2 != nil { dm.GroupV2 = msg.groupV2 } - + dm.ProfileKey = config.ProfileKey dm.Flags = &msg.flags return dm diff --git a/textsecure.go b/textsecure.go index fd465d1..6cbafef 100644 --- a/textsecure.go +++ b/textsecure.go @@ -19,6 +19,7 @@ import ( "github.com/golang/protobuf/proto" "github.com/signal-golang/textsecure/axolotl" + "github.com/signal-golang/textsecure/profiles" signalservice "github.com/signal-golang/textsecure/protobuf" rootCa "github.com/signal-golang/textsecure/rootCa" transport "github.com/signal-golang/textsecure/transport" @@ -309,6 +310,7 @@ func Setup(c *Client) error { if err != nil { return err } + client.RegistrationDone() rootCa.SetupCA(config.RootCA) transport.SetupTransporter(config.Server, config.Tel, registrationInfo.password, config.UserAgent, config.ProxyServer) @@ -317,6 +319,11 @@ func Setup(c *Client) error { identityKey, err = textSecureStore.GetIdentityKeyPair() // check if we have a uuid and if not get it config = checkUUID(config) + if len(config.ProfileKey) == 0 { + config.ProfileKey = profiles.GenerateProfileKey() + } + profiles.UpdateProfile(config.ProfileKey, config.UUID, "nanu") + GetProfile(config.UUID) return err } @@ -379,6 +386,8 @@ func registerDevice() error { if err != nil { return err } + config.ProfileKey = profiles.GenerateProfileKey() + config = checkUUID(config) client.RegistrationDone() if client.RegistrationDone != nil { log.Infoln("[textsecure] RegistrationDone") @@ -421,8 +430,10 @@ func handleMessage(srcE164 string, srcUUID string, timestamp uint64, b []byte) e return handleReceiptMessage(srcE164, srcUUID, timestamp, rm) } else if tm := content.GetTypingMessage(); tm != nil { return handleTypingMessage(srcE164, srcUUID, timestamp, tm) + } else if nm := content.GetNullMessage(); nm != nil { + log.Errorln("[textsecure] Nullmessage content received", content) + return nil } - //FIXME get the right content // log.Errorf(content) log.Errorln("[textsecure] Unknown message content received", content) diff --git a/transport/transport.go b/transport/transport.go index c02d905..e2795f0 100644 --- a/transport/transport.go +++ b/transport/transport.go @@ -129,7 +129,7 @@ func (ht *httpTransporter) Get(url string) (*response, error) { r.Body = resp.Body } - log.Debugf("GET %s %d\n", url, r.Status) + log.Debugf("[textsecure] GET %s %d\n", url, r.Status) return r, err }