summaryrefslogtreecommitdiffstats
path: root/vendor/go.mau.fi/whatsmeow/appstate
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/go.mau.fi/whatsmeow/appstate')
-rw-r--r--vendor/go.mau.fi/whatsmeow/appstate/decode.go310
-rw-r--r--vendor/go.mau.fi/whatsmeow/appstate/errors.go19
-rw-r--r--vendor/go.mau.fi/whatsmeow/appstate/hash.go96
-rw-r--r--vendor/go.mau.fi/whatsmeow/appstate/keys.go85
-rw-r--r--vendor/go.mau.fi/whatsmeow/appstate/lthash/lthash.go58
5 files changed, 568 insertions, 0 deletions
diff --git a/vendor/go.mau.fi/whatsmeow/appstate/decode.go b/vendor/go.mau.fi/whatsmeow/appstate/decode.go
new file mode 100644
index 00000000..5c895470
--- /dev/null
+++ b/vendor/go.mau.fi/whatsmeow/appstate/decode.go
@@ -0,0 +1,310 @@
+// Copyright (c) 2021 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 appstate
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+
+ "google.golang.org/protobuf/proto"
+
+ waBinary "go.mau.fi/whatsmeow/binary"
+ waProto "go.mau.fi/whatsmeow/binary/proto"
+ "go.mau.fi/whatsmeow/store"
+ "go.mau.fi/whatsmeow/util/cbcutil"
+)
+
+// PatchList represents a decoded response to getting app state patches from the WhatsApp servers.
+type PatchList struct {
+ Name WAPatchName
+ HasMorePatches bool
+ Patches []*waProto.SyncdPatch
+ Snapshot *waProto.SyncdSnapshot
+}
+
+// DownloadExternalFunc is a function that can download a blob of external app state patches.
+type DownloadExternalFunc func(*waProto.ExternalBlobReference) ([]byte, error)
+
+func parseSnapshotInternal(collection *waBinary.Node, downloadExternal DownloadExternalFunc) (*waProto.SyncdSnapshot, error) {
+ snapshotNode := collection.GetChildByTag("snapshot")
+ rawSnapshot, ok := snapshotNode.Content.([]byte)
+ if snapshotNode.Tag != "snapshot" || !ok {
+ return nil, nil
+ }
+ var snapshot waProto.ExternalBlobReference
+ err := proto.Unmarshal(rawSnapshot, &snapshot)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal snapshot: %w", err)
+ }
+ var rawData []byte
+ rawData, err = downloadExternal(&snapshot)
+ if err != nil {
+ return nil, fmt.Errorf("failed to download external mutations: %w", err)
+ }
+ var downloaded waProto.SyncdSnapshot
+ err = proto.Unmarshal(rawData, &downloaded)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal mutation list: %w", err)
+ }
+ return &downloaded, nil
+}
+
+func parsePatchListInternal(collection *waBinary.Node, downloadExternal DownloadExternalFunc) ([]*waProto.SyncdPatch, error) {
+ patchesNode := collection.GetChildByTag("patches")
+ patchNodes := patchesNode.GetChildren()
+ patches := make([]*waProto.SyncdPatch, 0, len(patchNodes))
+ for i, patchNode := range patchNodes {
+ rawPatch, ok := patchNode.Content.([]byte)
+ if patchNode.Tag != "patch" || !ok {
+ continue
+ }
+ var patch waProto.SyncdPatch
+ err := proto.Unmarshal(rawPatch, &patch)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal patch #%d: %w", i+1, err)
+ }
+ if patch.GetExternalMutations() != nil && downloadExternal != nil {
+ var rawData []byte
+ rawData, err = downloadExternal(patch.GetExternalMutations())
+ if err != nil {
+ return nil, fmt.Errorf("failed to download external mutations: %w", err)
+ }
+ var downloaded waProto.SyncdMutations
+ err = proto.Unmarshal(rawData, &downloaded)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal mutation list: %w", err)
+ } else if len(downloaded.GetMutations()) == 0 {
+ return nil, fmt.Errorf("didn't get any mutations from download")
+ }
+ patch.Mutations = downloaded.Mutations
+ }
+ patches = append(patches, &patch)
+ }
+ return patches, nil
+}
+
+// ParsePatchList will decode an XML node containing app state patches, including downloading any external blobs.
+func ParsePatchList(node *waBinary.Node, downloadExternal DownloadExternalFunc) (*PatchList, error) {
+ collection := node.GetChildByTag("sync", "collection")
+ ag := collection.AttrGetter()
+ snapshot, err := parseSnapshotInternal(&collection, downloadExternal)
+ if err != nil {
+ return nil, err
+ }
+ patches, err := parsePatchListInternal(&collection, downloadExternal)
+ if err != nil {
+ return nil, err
+ }
+ list := &PatchList{
+ Name: WAPatchName(ag.String("name")),
+ HasMorePatches: ag.OptionalBool("has_more_patches"),
+ Patches: patches,
+ Snapshot: snapshot,
+ }
+ return list, ag.Error()
+}
+
+type patchOutput struct {
+ RemovedMACs [][]byte
+ AddedMACs []store.AppStateMutationMAC
+ Mutations []Mutation
+}
+
+func (proc *Processor) decodeMutations(mutations []*waProto.SyncdMutation, out *patchOutput, validateMACs bool) error {
+ for i, mutation := range mutations {
+ keyID := mutation.GetRecord().GetKeyId().GetId()
+ keys, err := proc.getAppStateKey(keyID)
+ if err != nil {
+ return fmt.Errorf("failed to get key %X to decode mutation: %w", keyID, err)
+ }
+ content := mutation.GetRecord().GetValue().GetBlob()
+ content, valueMAC := content[:len(content)-32], content[len(content)-32:]
+ if validateMACs {
+ expectedValueMAC := generateContentMAC(mutation.GetOperation(), content, keyID, keys.ValueMAC)
+ if !bytes.Equal(expectedValueMAC, valueMAC) {
+ return fmt.Errorf("failed to verify mutation #%d: %w", i+1, ErrMismatchingContentMAC)
+ }
+ }
+ iv, content := content[:16], content[16:]
+ plaintext, err := cbcutil.Decrypt(keys.ValueEncryption, iv, content)
+ if err != nil {
+ return fmt.Errorf("failed to decrypt mutation #%d: %w", i+1, err)
+ }
+ var syncAction waProto.SyncActionData
+ err = proto.Unmarshal(plaintext, &syncAction)
+ if err != nil {
+ return fmt.Errorf("failed to unmarshal mutation #%d: %w", i+1, err)
+ }
+ indexMAC := mutation.GetRecord().GetIndex().GetBlob()
+ if validateMACs {
+ expectedIndexMAC := concatAndHMAC(sha256.New, keys.Index, syncAction.Index)
+ if !bytes.Equal(expectedIndexMAC, indexMAC) {
+ return fmt.Errorf("failed to verify mutation #%d: %w", i+1, ErrMismatchingIndexMAC)
+ }
+ }
+ var index []string
+ err = json.Unmarshal(syncAction.GetIndex(), &index)
+ if err != nil {
+ return fmt.Errorf("failed to unmarshal index of mutation #%d: %w", i+1, err)
+ }
+ if mutation.GetOperation() == waProto.SyncdMutation_REMOVE {
+ out.RemovedMACs = append(out.RemovedMACs, indexMAC)
+ } else if mutation.GetOperation() == waProto.SyncdMutation_SET {
+ out.AddedMACs = append(out.AddedMACs, store.AppStateMutationMAC{
+ IndexMAC: indexMAC,
+ ValueMAC: valueMAC,
+ })
+ }
+ out.Mutations = append(out.Mutations, Mutation{
+ Operation: mutation.GetOperation(),
+ Action: syncAction.GetValue(),
+ Index: index,
+ IndexMAC: indexMAC,
+ ValueMAC: valueMAC,
+ })
+ }
+ return nil
+}
+
+func (proc *Processor) storeMACs(name WAPatchName, currentState HashState, out *patchOutput) {
+ err := proc.Store.AppState.PutAppStateVersion(string(name), currentState.Version, currentState.Hash)
+ if err != nil {
+ proc.Log.Errorf("Failed to update app state version in the database: %v", err)
+ }
+ err = proc.Store.AppState.DeleteAppStateMutationMACs(string(name), out.RemovedMACs)
+ if err != nil {
+ proc.Log.Errorf("Failed to remove deleted mutation MACs from the database: %v", err)
+ }
+ err = proc.Store.AppState.PutAppStateMutationMACs(string(name), currentState.Version, out.AddedMACs)
+ if err != nil {
+ proc.Log.Errorf("Failed to insert added mutation MACs to the database: %v", err)
+ }
+}
+
+func (proc *Processor) validateSnapshotMAC(name WAPatchName, currentState HashState, keyID, expectedSnapshotMAC []byte) (keys ExpandedAppStateKeys, err error) {
+ keys, err = proc.getAppStateKey(keyID)
+ if err != nil {
+ err = fmt.Errorf("failed to get key %X to verify patch v%d MACs: %w", keyID, currentState.Version, err)
+ return
+ }
+ snapshotMAC := currentState.generateSnapshotMAC(name, keys.SnapshotMAC)
+ if !bytes.Equal(snapshotMAC, expectedSnapshotMAC) {
+ err = fmt.Errorf("failed to verify patch v%d: %w", currentState.Version, ErrMismatchingLTHash)
+ }
+ return
+}
+
+func (proc *Processor) decodeSnapshot(name WAPatchName, ss *waProto.SyncdSnapshot, initialState HashState, validateMACs bool, newMutationsInput []Mutation) (newMutations []Mutation, currentState HashState, err error) {
+ currentState = initialState
+ currentState.Version = ss.GetVersion().GetVersion()
+
+ encryptedMutations := make([]*waProto.SyncdMutation, len(ss.GetRecords()))
+ for i, record := range ss.GetRecords() {
+ encryptedMutations[i] = &waProto.SyncdMutation{
+ Operation: waProto.SyncdMutation_SET.Enum(),
+ Record: record,
+ }
+ }
+
+ var warn []error
+ warn, err = currentState.updateHash(encryptedMutations, func(indexMAC []byte, maxIndex int) ([]byte, error) {
+ return nil, nil
+ })
+ if len(warn) > 0 {
+ proc.Log.Warnf("Warnings while updating hash for %s: %+v", name, warn)
+ }
+ if err != nil {
+ err = fmt.Errorf("failed to update state hash: %w", err)
+ return
+ }
+
+ if validateMACs {
+ _, err = proc.validateSnapshotMAC(name, currentState, ss.GetKeyId().GetId(), ss.GetMac())
+ if err != nil {
+ return
+ }
+ }
+
+ var out patchOutput
+ out.Mutations = newMutationsInput
+ err = proc.decodeMutations(encryptedMutations, &out, validateMACs)
+ if err != nil {
+ err = fmt.Errorf("failed to decode snapshot of v%d: %w", currentState.Version, err)
+ return
+ }
+ proc.storeMACs(name, currentState, &out)
+ newMutations = out.Mutations
+ return
+}
+
+// DecodePatches will decode all the patches in a PatchList into a list of app state mutations.
+func (proc *Processor) DecodePatches(list *PatchList, initialState HashState, validateMACs bool) (newMutations []Mutation, currentState HashState, err error) {
+ currentState = initialState
+ var expectedLength int
+ if list.Snapshot != nil {
+ expectedLength = len(list.Snapshot.GetRecords())
+ }
+ for _, patch := range list.Patches {
+ expectedLength += len(patch.GetMutations())
+ }
+ newMutations = make([]Mutation, 0, expectedLength)
+
+ if list.Snapshot != nil {
+ newMutations, currentState, err = proc.decodeSnapshot(list.Name, list.Snapshot, currentState, validateMACs, newMutations)
+ if err != nil {
+ return
+ }
+ }
+
+ for _, patch := range list.Patches {
+ version := patch.GetVersion().GetVersion()
+ currentState.Version = version
+ var warn []error
+ warn, err = currentState.updateHash(patch.GetMutations(), func(indexMAC []byte, maxIndex int) ([]byte, error) {
+ for i := maxIndex - 1; i >= 0; i-- {
+ if bytes.Equal(patch.Mutations[i].GetRecord().GetIndex().GetBlob(), indexMAC) {
+ value := patch.Mutations[i].GetRecord().GetValue().GetBlob()
+ return value[len(value)-32:], nil
+ }
+ }
+ // Previous value not found in current patch, look in the database
+ return proc.Store.AppState.GetAppStateMutationMAC(string(list.Name), indexMAC)
+ })
+ if len(warn) > 0 {
+ proc.Log.Warnf("Warnings while updating hash for %s: %+v", list.Name, warn)
+ }
+ if err != nil {
+ err = fmt.Errorf("failed to update state hash: %w", err)
+ return
+ }
+
+ if validateMACs {
+ var keys ExpandedAppStateKeys
+ keys, err = proc.validateSnapshotMAC(list.Name, currentState, patch.GetKeyId().GetId(), patch.GetSnapshotMac())
+ if err != nil {
+ return
+ }
+ patchMAC := generatePatchMAC(patch, list.Name, keys.PatchMAC)
+ if !bytes.Equal(patchMAC, patch.GetPatchMac()) {
+ err = fmt.Errorf("failed to verify patch v%d: %w", version, ErrMismatchingPatchMAC)
+ return
+ }
+ }
+
+ var out patchOutput
+ out.Mutations = newMutations
+ err = proc.decodeMutations(patch.GetMutations(), &out, validateMACs)
+ if err != nil {
+ return
+ }
+ proc.storeMACs(list.Name, currentState, &out)
+ newMutations = out.Mutations
+ }
+ return
+}
diff --git a/vendor/go.mau.fi/whatsmeow/appstate/errors.go b/vendor/go.mau.fi/whatsmeow/appstate/errors.go
new file mode 100644
index 00000000..0668d86a
--- /dev/null
+++ b/vendor/go.mau.fi/whatsmeow/appstate/errors.go
@@ -0,0 +1,19 @@
+// Copyright (c) 2021 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 appstate
+
+import "errors"
+
+// Errors that this package can return.
+var (
+ ErrMissingPreviousSetValueOperation = errors.New("missing value MAC of previous SET operation")
+ ErrMismatchingLTHash = errors.New("mismatching LTHash")
+ ErrMismatchingPatchMAC = errors.New("mismatching patch MAC")
+ ErrMismatchingContentMAC = errors.New("mismatching content MAC")
+ ErrMismatchingIndexMAC = errors.New("mismatching index MAC")
+ ErrKeyNotFound = errors.New("didn't find app state key")
+)
diff --git a/vendor/go.mau.fi/whatsmeow/appstate/hash.go b/vendor/go.mau.fi/whatsmeow/appstate/hash.go
new file mode 100644
index 00000000..4fdbabf6
--- /dev/null
+++ b/vendor/go.mau.fi/whatsmeow/appstate/hash.go
@@ -0,0 +1,96 @@
+// Copyright (c) 2021 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 appstate
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/binary"
+ "fmt"
+ "hash"
+
+ "go.mau.fi/whatsmeow/appstate/lthash"
+ waProto "go.mau.fi/whatsmeow/binary/proto"
+)
+
+type Mutation struct {
+ Operation waProto.SyncdMutation_SyncdMutationSyncdOperation
+ Action *waProto.SyncActionValue
+ Index []string
+ IndexMAC []byte
+ ValueMAC []byte
+}
+
+type HashState struct {
+ Version uint64
+ Hash [128]byte
+}
+
+func (hs *HashState) updateHash(mutations []*waProto.SyncdMutation, getPrevSetValueMAC func(indexMAC []byte, maxIndex int) ([]byte, error)) ([]error, error) {
+ var added, removed [][]byte
+ var warnings []error
+
+ for i, mutation := range mutations {
+ if mutation.GetOperation() == waProto.SyncdMutation_SET {
+ value := mutation.GetRecord().GetValue().GetBlob()
+ added = append(added, value[len(value)-32:])
+ }
+ indexMAC := mutation.GetRecord().GetIndex().GetBlob()
+ removal, err := getPrevSetValueMAC(indexMAC, i)
+ if err != nil {
+ return warnings, fmt.Errorf("failed to get value MAC of previous SET operation: %w", err)
+ } else if removal != nil {
+ removed = append(removed, removal)
+ } else if mutation.GetOperation() == waProto.SyncdMutation_REMOVE {
+ // TODO figure out if there are certain cases that are safe to ignore and others that aren't
+ // At least removing contact access from WhatsApp seems to create a REMOVE op for your own JID
+ // that points to a non-existent index and is safe to ignore here. Other keys might not be safe to ignore.
+ warnings = append(warnings, fmt.Errorf("%w for %X", ErrMissingPreviousSetValueOperation, indexMAC))
+ //return ErrMissingPreviousSetValueOperation
+ }
+ }
+
+ lthash.WAPatchIntegrity.SubtractThenAddInPlace(hs.Hash[:], removed, added)
+ return warnings, nil
+}
+
+func uint64ToBytes(val uint64) []byte {
+ data := make([]byte, 8)
+ binary.BigEndian.PutUint64(data, val)
+ return data
+}
+
+func concatAndHMAC(alg func() hash.Hash, key []byte, data ...[]byte) []byte {
+ h := hmac.New(alg, key)
+ for _, item := range data {
+ h.Write(item)
+ }
+ return h.Sum(nil)
+}
+
+func (hs *HashState) generateSnapshotMAC(name WAPatchName, key []byte) []byte {
+ return concatAndHMAC(sha256.New, key, hs.Hash[:], uint64ToBytes(hs.Version), []byte(name))
+}
+
+func generatePatchMAC(patch *waProto.SyncdPatch, name WAPatchName, key []byte) []byte {
+ dataToHash := make([][]byte, len(patch.GetMutations())+3)
+ dataToHash[0] = patch.GetSnapshotMac()
+ for i, mutation := range patch.Mutations {
+ val := mutation.GetRecord().GetValue().GetBlob()
+ dataToHash[i+1] = val[len(val)-32:]
+ }
+ dataToHash[len(dataToHash)-2] = uint64ToBytes(patch.GetVersion().GetVersion())
+ dataToHash[len(dataToHash)-1] = []byte(name)
+ return concatAndHMAC(sha256.New, key, dataToHash...)
+}
+
+func generateContentMAC(operation waProto.SyncdMutation_SyncdMutationSyncdOperation, data, keyID, key []byte) []byte {
+ operationBytes := []byte{byte(operation) + 1}
+ keyDataLength := uint64ToBytes(uint64(len(keyID) + 1))
+ return concatAndHMAC(sha512.New, key, operationBytes, keyID, data, keyDataLength)[:32]
+}
diff --git a/vendor/go.mau.fi/whatsmeow/appstate/keys.go b/vendor/go.mau.fi/whatsmeow/appstate/keys.go
new file mode 100644
index 00000000..30ecbcc4
--- /dev/null
+++ b/vendor/go.mau.fi/whatsmeow/appstate/keys.go
@@ -0,0 +1,85 @@
+// Copyright (c) 2021 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 appstate implements encoding and decoding WhatsApp's app state patches.
+package appstate
+
+import (
+ "encoding/base64"
+ "sync"
+
+ "go.mau.fi/whatsmeow/store"
+ "go.mau.fi/whatsmeow/util/hkdfutil"
+ waLog "go.mau.fi/whatsmeow/util/log"
+)
+
+// WAPatchName represents a type of app state patch.
+type WAPatchName string
+
+const (
+ // WAPatchCriticalBlock contains the user's settings like push name and locale.
+ WAPatchCriticalBlock WAPatchName = "critical_block"
+ // WAPatchCriticalUnblockLow contains the user's contact list.
+ WAPatchCriticalUnblockLow WAPatchName = "critical_unblock_low"
+ // WAPatchRegularLow contains some local chat settings like pin, archive status, and the setting of whether to unarchive chats when messages come in.
+ WAPatchRegularLow WAPatchName = "regular_low"
+ // WAPatchRegularHigh contains more local chat settings like mute status and starred messages.
+ WAPatchRegularHigh WAPatchName = "regular_high"
+ // WAPatchRegular contains protocol info about app state patches like key expiration.
+ WAPatchRegular WAPatchName = "regular"
+)
+
+// AllPatchNames contains all currently known patch state names.
+var AllPatchNames = [...]WAPatchName{WAPatchCriticalBlock, WAPatchCriticalUnblockLow, WAPatchRegularHigh, WAPatchRegular, WAPatchRegularLow}
+
+type Processor struct {
+ keyCache map[string]ExpandedAppStateKeys
+ keyCacheLock sync.Mutex
+ Store *store.Device
+ Log waLog.Logger
+}
+
+func NewProcessor(store *store.Device, log waLog.Logger) *Processor {
+ return &Processor{
+ keyCache: make(map[string]ExpandedAppStateKeys),
+ Store: store,
+ Log: log,
+ }
+}
+
+type ExpandedAppStateKeys struct {
+ Index []byte
+ ValueEncryption []byte
+ ValueMAC []byte
+ SnapshotMAC []byte
+ PatchMAC []byte
+}
+
+func expandAppStateKeys(keyData []byte) (keys ExpandedAppStateKeys) {
+ appStateKeyExpanded := hkdfutil.SHA256(keyData, nil, []byte("WhatsApp Mutation Keys"), 160)
+ return ExpandedAppStateKeys{appStateKeyExpanded[0:32], appStateKeyExpanded[32:64], appStateKeyExpanded[64:96], appStateKeyExpanded[96:128], appStateKeyExpanded[128:160]}
+}
+
+func (proc *Processor) getAppStateKey(keyID []byte) (keys ExpandedAppStateKeys, err error) {
+ keyCacheID := base64.RawStdEncoding.EncodeToString(keyID)
+ var ok bool
+
+ proc.keyCacheLock.Lock()
+ defer proc.keyCacheLock.Unlock()
+
+ keys, ok = proc.keyCache[keyCacheID]
+ if !ok {
+ var keyData *store.AppStateSyncKey
+ keyData, err = proc.Store.AppStateKeys.GetAppStateSyncKey(keyID)
+ if keyData != nil {
+ keys = expandAppStateKeys(keyData.Data)
+ proc.keyCache[keyCacheID] = keys
+ } else if err == nil {
+ err = ErrKeyNotFound
+ }
+ }
+ return
+}
diff --git a/vendor/go.mau.fi/whatsmeow/appstate/lthash/lthash.go b/vendor/go.mau.fi/whatsmeow/appstate/lthash/lthash.go
new file mode 100644
index 00000000..8d3045d6
--- /dev/null
+++ b/vendor/go.mau.fi/whatsmeow/appstate/lthash/lthash.go
@@ -0,0 +1,58 @@
+// Copyright (c) 2021 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 lthash implements a summation based hash algorithm that maintains the
+// integrity of a piece of data over a series of mutations. You can add/remove
+// mutations, and it'll return a hash equal to if the same series of mutations
+// was made sequentially.
+package lthash
+
+import (
+ "encoding/binary"
+
+ "go.mau.fi/whatsmeow/util/hkdfutil"
+)
+
+type LTHash struct {
+ HKDFInfo []byte
+ HKDFSize uint8
+}
+
+// WAPatchIntegrity is a LTHash instance initialized with the details used for verifying integrity of WhatsApp app state sync patches.
+var WAPatchIntegrity = LTHash{[]byte("WhatsApp Patch Integrity"), 128}
+
+func (lth LTHash) SubtractThenAdd(base []byte, subtract, add [][]byte) []byte {
+ output := make([]byte, len(base))
+ copy(output, base)
+ lth.SubtractThenAddInPlace(output, subtract, add)
+ return output
+}
+
+func (lth LTHash) SubtractThenAddInPlace(base []byte, subtract, add [][]byte) {
+ lth.multipleOp(base, subtract, true)
+ lth.multipleOp(base, add, false)
+}
+
+func (lth LTHash) multipleOp(base []byte, input [][]byte, subtract bool) {
+ for _, item := range input {
+ performPointwiseWithOverflow(base, hkdfutil.SHA256(item, nil, lth.HKDFInfo, lth.HKDFSize), subtract)
+ }
+}
+
+func performPointwiseWithOverflow(base, input []byte, subtract bool) []byte {
+ for i := 0; i < len(base); i += 2 {
+ x := binary.LittleEndian.Uint16(base[i : i+2])
+ y := binary.LittleEndian.Uint16(input[i : i+2])
+ var result uint16
+ if subtract {
+ result = x - y
+ } else {
+ result = x + y
+ }
+ binary.LittleEndian.PutUint16(base[i:i+2], result)
+ }
+ return base
+}