All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.auties.whatsapp.socket.AppStateHandler Maven / Gradle / Ivy

There is a newer version: 3.5.1
Show newest version
package it.auties.whatsapp.socket;

import it.auties.whatsapp.api.ClientType;
import it.auties.whatsapp.binary.BinaryPatchType;
import it.auties.whatsapp.crypto.AesCbc;
import it.auties.whatsapp.crypto.Hmac;
import it.auties.whatsapp.crypto.LTHash;
import it.auties.whatsapp.exception.HmacValidationException;
import it.auties.whatsapp.model.action.*;
import it.auties.whatsapp.model.chat.Chat;
import it.auties.whatsapp.model.chat.ChatMute;
import it.auties.whatsapp.model.contact.Contact;
import it.auties.whatsapp.model.contact.ContactJid;
import it.auties.whatsapp.model.info.MessageIndexInfo;
import it.auties.whatsapp.model.info.MessageInfo;
import it.auties.whatsapp.model.request.Attributes;
import it.auties.whatsapp.model.request.Node;
import it.auties.whatsapp.model.setting.EphemeralSetting;
import it.auties.whatsapp.model.setting.LocaleSetting;
import it.auties.whatsapp.model.setting.PushNameSetting;
import it.auties.whatsapp.model.setting.UnarchiveChatsSetting;
import it.auties.whatsapp.model.sync.*;
import it.auties.whatsapp.model.sync.PatchRequest.PatchEntry;
import it.auties.whatsapp.util.*;
import lombok.NonNull;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static it.auties.whatsapp.api.ErrorHandler.Location.*;
import static java.lang.System.Logger.Level.WARNING;

class AppStateHandler {
    private static final int TIMEOUT = 120;
    private static final int PULL_ATTEMPTS = 3;
    private static final boolean CHECK_MACS = true;

    private final SocketHandler socketHandler;
    private final Map attempts;
    private ExecutorService executor;

    protected AppStateHandler(SocketHandler socketHandler) {
        this.socketHandler = socketHandler;
        this.attempts = new HashMap<>();
    }

    private synchronized ExecutorService getOrCreateAppService(){
        if(executor == null || executor.isShutdown()){
            executor = Executors.newSingleThreadExecutor();
        }

        return executor;
    }

    protected CompletableFuture push(@NonNull ContactJid jid, @NonNull List patches) {
        return runPushTask(() -> {
            var clientType = socketHandler.store().clientType();
            var pullOperation = switch (clientType){
                case MOBILE -> CompletableFuture.completedFuture(null);
                case WEB -> pullUninterruptedly(jid, getPatchesTypes(patches));
            };
            return pullOperation.thenComposeAsync(ignored -> sendPush(jid, patches, clientType != ClientType.MOBILE))
                    .orTimeout(TIMEOUT, TimeUnit.SECONDS)
                    .exceptionallyAsync(throwable -> socketHandler.handleFailure(PUSH_APP_STATE, throwable));
        });
    }

    private Set getPatchesTypes(List patches) {
        return patches.stream()
                .map(PatchRequest::type)
                .collect(Collectors.toUnmodifiableSet());
    }

    private CompletableFuture runPushTask(Supplier> task){
        var executor = getOrCreateAppService();
        var future = new CompletableFuture();
        executor.execute(() -> {
            try {
                task.get().join();
                future.complete(null);
            }catch (Throwable throwable){
                future.completeExceptionally(throwable);
            }
        });
        return future;
    }

    private CompletableFuture sendPush(ContactJid jid, List patches, boolean readPatches) {
        var requests = patches.stream()
                .map(entry -> createPushRequest(jid, entry))
                .toList();
        var mobile = socketHandler.store().clientType() == ClientType.MOBILE;
        var body = requests.stream()
                .map(request -> createPushRequestNode(request, mobile))
                .toList();
        var syncAttributes = Attributes.of()
                .put("data_namespace", 3, mobile)
                .toMap();
        var sync = Node.ofChildren("sync", syncAttributes, body);
        return socketHandler.sendQuery("set", "w:sync:app:state", sync)
                .thenRunAsync(() -> onPush(jid, requests, readPatches));
    }

    private PushRequest createPushRequest(ContactJid jid, PatchRequest request) {
        var oldState = socketHandler.keys()
                .findHashStateByName(jid, request.type())
                .orElseGet(() -> new LTHashState(request.type()));
        var newState = oldState.copy();
        var key = socketHandler.keys().getLatestAppKey(jid);
        var mutationKeys = MutationKeys.of(key.keyData().keyData());
        var syncId = new KeyId(key.keyId().keyId());
        var mutations = request.entries()
                .stream()
                .map(patch -> createMutationSync(patch, mutationKeys, key, syncId))
                .toList();
        var newStateGenerator = new LTHash(newState);
        mutations.forEach(mutation -> newStateGenerator.mix(mutation.indexMac(), mutation.valueMac(), mutation.operation()));
        var result = newStateGenerator.finish();
        newState.hash(result.hash());
        newState.indexValueMap(result.indexValueMap());
        newState.version(newState.version() + 1);
        var snapshotMac = generateSnapshotMac(newState.hash(), newState.version(), request.type(), mutationKeys.snapshotMacKey());
        var concatValueMac = mutations.stream()
                .map(MutationResult::valueMac)
                .toArray(byte[][]::new);
        var patchMac = generatePatchMac(snapshotMac, concatValueMac, newState.version(), request.type(), mutationKeys.patchMacKey());
        var syncs = mutations.stream()
                .map(MutationResult::sync)
                .toList();
        var sync = PatchSync.builder()
                .patchMac(patchMac)
                .snapshotMac(snapshotMac)
                .keyId(syncId)
                .mutations(syncs)
                .build();
        return new PushRequest(request.type(), oldState, newState, sync);
    }

    private MutationResult createMutationSync(PatchEntry patch, MutationKeys mutationKeys, AppStateSyncKey key, KeyId syncId) {
        var index = patch.index().getBytes(StandardCharsets.UTF_8);
        var actionData = ActionDataSync.builder()
                .index(index)
                .value(patch.sync())
                .padding(new byte[0])
                .version(patch.version())
                .build();
        var encoded = Protobuf.writeMessage(actionData);
        var encrypted = AesCbc.encryptAndPrefix(encoded, mutationKeys.encKey());
        var valueMac = generateMac(patch.operation(), encrypted, key.keyId().keyId(), mutationKeys.macKey());
        var indexMac = Hmac.calculateSha256(index, mutationKeys.indexKey());
        var record = RecordSync.builder()
                .index(new IndexSync(indexMac))
                .value(new ValueSync(BytesHelper.concat(encrypted, valueMac)))
                .keyId(syncId)
                .build();
        var sync = MutationSync.builder()
                .operation(patch.operation())
                .record(record)
                .build();
        return new MutationResult(sync, indexMac, valueMac, patch.operation());
    }

    private Node createPushRequestNode(PushRequest request, boolean mobile) {
        var version = request.oldState().version();
        var collectionAttributes = Attributes.of()
                .put("name", request.type())
                .put("version", version, !mobile)
                .put("return_snapshot", false, !mobile)
                .put("order", request.type() != BinaryPatchType.CRITICAL_UNBLOCK_LOW ? "1" : "0", mobile)
                .toMap();
        return Node.ofChildren("collection", collectionAttributes,
                Node.of("patch", Protobuf.writeMessage(request.sync())));
    }

    private void onPush(ContactJid jid, List requests, boolean readPatches) {
        requests.forEach(request -> {
            socketHandler.keys().putState(jid, request.newState());
            if (!readPatches) {
                return;
            }

            var patches = List.of(request.sync().withVersion(new VersionSync(request.newState().version())));
            var results = decodePatches(jid, request.type(), patches, request.oldState());
            results.records().forEach(this::processActions);
        });
    }

    protected void pull(BinaryPatchType... patchTypes) {
        if (patchTypes == null || patchTypes.length == 0) {
            return;
        }

        getOrCreateAppService().execute(() -> pullUninterruptedly(socketHandler.store().jid(), Set.of(patchTypes))
                .thenAcceptAsync(success -> onPull(false, success))
                .exceptionallyAsync(exception -> onPullError(false, exception)));
    }

    protected CompletableFuture pullInitial() {
        if(socketHandler.store().initialSync()){
            return CompletableFuture.completedFuture(null);
        }

        return pullUninterruptedly(socketHandler.store().jid(), Set.of(BinaryPatchType.values()))
                .thenAcceptAsync(success -> onPull(true, success))
                .exceptionallyAsync(exception -> onPullError(true, exception));
    }

    private void onPull(boolean initial, boolean success) {
        if (!socketHandler.store().initialSync()) {
            socketHandler.store().initialSync((initial && success) || isSyncComplete());
        }

        attempts.clear();
    }

    private boolean isSyncComplete() {
        return Arrays.stream(BinaryPatchType.values())
                .allMatch(this::isSyncComplete);
    }

    private boolean isSyncComplete(BinaryPatchType entry) {
        return socketHandler.keys()
                .findHashStateByName(socketHandler.store().jid(), entry) // FIXME: Is this right?
                .filter(type -> type.version() > 0)
                .isPresent();
    }

    private Void onPullError(boolean initial, Throwable exception) {
        attempts.clear();
        if (initial) {
            return socketHandler.handleFailure(INITIAL_APP_STATE_SYNC, exception);
        }
        return socketHandler.handleFailure(PULL_APP_STATE, exception);
    }

    private CompletableFuture pullUninterruptedly(ContactJid jid, Set patchTypes) {
        var tempStates = new HashMap();
        var nodes = getPullNodes(jid, patchTypes, tempStates);
        return socketHandler.sendQuery("set", "w:sync:app:state", Node.ofChildren("sync", nodes))
                .thenApplyAsync(this::parseSyncRequest)
                .thenApplyAsync(records -> decodeSyncs(jid, tempStates, records))
                .thenComposeAsync(remaining -> handlePullResult(jid, remaining))
                .orTimeout(TIMEOUT, TimeUnit.SECONDS);
    }

    private CompletableFuture handlePullResult(ContactJid jid, Set remaining) {
        return remaining.isEmpty() ? CompletableFuture.completedFuture(true) : pullUninterruptedly(jid, remaining);
    }

    private List getPullNodes(ContactJid jid, Set patchTypes, Map tempStates) {
        return patchTypes.stream()
                .map(name -> createStateWithVersion(jid, name))
                .peek(state -> tempStates.put(state.name(), state))
                .map(LTHashState::toNode)
                .toList();
    }

    private LTHashState createStateWithVersion(ContactJid jid, BinaryPatchType name) {
        return socketHandler.keys()
                .findHashStateByName(jid, name)
                .orElseGet(() -> new LTHashState(name));
    }

    private Set decodeSyncs(ContactJid jid, Map tempStates, List records) {
        return records.stream()
                .map(record -> {
                    var chunk = decodeSync(jid, record, tempStates);
                    chunk.records().forEach(this::processActions);
                    return chunk;
                })
                .filter(PatchChunk::hasMore)
                .map(PatchChunk::patchType)
                .collect(Collectors.toUnmodifiableSet());
    }

    private PatchChunk decodeSync(ContactJid jid, SnapshotSyncRecord record, Map tempStates) {
        try {
            var results = new ArrayList();
            if (record.hasSnapshot()) {
                var snapshot = decodeSnapshot(jid, record.patchType(), record.snapshot());
                snapshot.ifPresent(decodedSnapshot -> {
                    results.addAll(decodedSnapshot.records());
                    tempStates.put(record.patchType(), decodedSnapshot.state());
                    socketHandler.keys().putState(jid, decodedSnapshot.state());
                });
            }
            if (record.hasPatches()) {
                var decodedPatches = decodePatches(jid, record.patchType(), record.patches(), tempStates.get(record.patchType()));
                results.addAll(decodedPatches.records());
                socketHandler.keys().putState(jid, decodedPatches.state());
            }
            return new PatchChunk(record.patchType(), results, record.hasMore());
        } catch (Throwable throwable) {
            var hashState = new LTHashState(record.patchType());
            socketHandler.keys().putState(jid, hashState);
            attempts.put(record.patchType(), attempts.getOrDefault(record.patchType(), 0) + 1);
            if (attempts.get(record.patchType()) >= PULL_ATTEMPTS) {
                throw new RuntimeException("Cannot parse patch(%s tries)".formatted(PULL_ATTEMPTS), throwable);
            }
            return decodeSync(jid, record, tempStates);
        }
    }

    private List parseSyncRequest(Node node) {
        return Stream.ofNullable(node)
                .map(sync -> sync.findNodes("sync"))
                .flatMap(Collection::stream)
                .map(sync -> sync.findNodes("collection"))
                .flatMap(Collection::stream)
                .map(this::parseSync)
                .flatMap(Optional::stream)
                .toList();
    }

    private Optional parseSync(Node sync) {
        var name = BinaryPatchType.of(sync.attributes().getString("name"));
        var type = sync.attributes().getString("type");
        if (Objects.equals(type, "error")) {
            return Optional.empty();
        }
        var more = sync.attributes().getBoolean("has_more_patches");
        var snapshotSync = sync.findNode("snapshot")
                .flatMap(this::decodeSnapshot)
                .orElse(null);
        var versionCode = sync.attributes().getInt("version");
        var patches = sync.findNode("patches")
                .orElse(sync)
                .findNodes("patch")
                .stream()
                .map(patch -> decodePatch(patch, versionCode))
                .flatMap(Optional::stream)
                .toList();
        return Optional.of(new SnapshotSyncRecord(name, snapshotSync, patches, more));
    }

    private Optional decodeSnapshot(Node snapshot) {
        return snapshot == null ? Optional.empty() : snapshot.contentAsBytes()
                .map(bytes -> Protobuf.readMessage(bytes, ExternalBlobReference.class))
                .map(Medias::download)
                .flatMap(CompletableFuture::join)
                .map(value -> Protobuf.readMessage(value, SnapshotSync.class));
    }

    private Optional decodePatch(Node patch, long versionCode) {
        if (!patch.hasContent()) {
            return Optional.empty();
        }
        var patchSync = Protobuf.readMessage(patch.contentAsBytes().orElseThrow(), PatchSync.class);
        if (!patchSync.hasVersion()) {
            var version = new VersionSync(versionCode + 1);
            patchSync.version(version);
        }
        return Optional.of(patchSync);
    }

    private void processActions(ActionDataSync mutation) {
        var value = mutation.value();
        if (value == null) {
            return;
        }
        var action = value.action();
        if (action != null) {
            var messageIndex = mutation.messageIndex();
            var targetContact = messageIndex.chatJid().flatMap(socketHandler.store()::findContactByJid);
            var targetChat = messageIndex.chatJid().flatMap(socketHandler.store()::findChatByJid);
            var targetMessage = targetChat.flatMap(chat -> socketHandler.store()
                    .findMessageById(chat, mutation.messageIndex().messageId().orElse(null)));
            if (action instanceof ClearChatAction clearChatAction) {
                clearMessages(targetChat.orElse(null), clearChatAction);
            } else if (action instanceof ContactAction contactAction) {
                updateName(targetContact.orElseGet(() -> createContact(messageIndex)), targetChat.orElseGet(() -> createChat(messageIndex)), contactAction);
            } else if (action instanceof DeleteChatAction) {
                targetChat.ifPresent(Chat::removeMessages);
            } else if (action instanceof DeleteMessageForMeAction) {
                targetMessage.ifPresent(message -> targetChat.ifPresent(chat -> deleteMessage(message, chat)));
            } else if (action instanceof MarkChatAsReadAction markAction) {
                targetChat.ifPresent(chat -> chat.unreadMessagesCount(markAction.read() ? 0 : -1));
            } else if (action instanceof MuteAction muteAction) {
                targetChat.ifPresent(chat -> chat.mute(ChatMute.muted(muteAction.muteEndTimestampSeconds())));
            } else if (action instanceof PinAction pinAction) {
                targetChat.ifPresent(chat -> chat.pinnedTimestampSeconds(pinAction.pinned() ? (int) mutation.value()
                        .timestamp() : 0));
            } else if (action instanceof StarAction starAction) {
                targetMessage.ifPresent(message -> message.starred(starAction.starred()));
            } else if (action instanceof ArchiveChatAction archiveChatAction) {
                targetChat.ifPresent(chat -> chat.archived(archiveChatAction.archived()));
            } else if (action instanceof TimeFormatAction timeFormatAction) {
                socketHandler.store().twentyFourHourFormat(timeFormatAction.twentyFourHourFormatEnabled());
            }
            socketHandler.onAction(action, messageIndex);
        }
        var setting = value.setting();
        if (setting != null) {
            if (setting instanceof EphemeralSetting ephemeralSetting) {
                showEphemeralMessageWarning(ephemeralSetting);
            } else if (setting instanceof LocaleSetting localeSetting) {
                socketHandler.updateLocale(localeSetting.locale(), socketHandler.store().locale());
            } else if (setting instanceof PushNameSetting pushNameSetting) {
                socketHandler.updateUserName(pushNameSetting.name(), socketHandler.store().name());
            } else if (setting instanceof UnarchiveChatsSetting unarchiveChatsSetting) {
                socketHandler.store().unarchiveChats(unarchiveChatsSetting.unarchiveChats());
            }
            socketHandler.onSetting(setting);
        }
        var features = mutation.value().primaryFeature();
        if (features.isPresent() && !features.get().flags().isEmpty()) {
            socketHandler.onFeatures(features.get());
        }
    }

    private Chat createChat(MessageIndexInfo messageIndex) {
        var chat = messageIndex.chatJid().orElseThrow();
        return socketHandler.store().addNewChat(chat);
    }

    private Contact createContact(MessageIndexInfo messageIndex) {
        var chatJid = messageIndex.chatJid().orElseThrow();
        var contact = socketHandler.store().addContact(chatJid);
        socketHandler.onNewContact(contact);
        return contact;
    }

    private void showEphemeralMessageWarning(EphemeralSetting ephemeralSetting) {
        var logger = System.getLogger("AppStateHandler");
        logger.log(WARNING, "An ephemeral status update was received as a setting. " + "Data: %s".formatted(ephemeralSetting) + "This should not be possible." + " Open an issue on Github please");
    }

    private void clearMessages(Chat targetChat, ClearChatAction clearChatAction) {
        if (targetChat == null) {
            return;
        }

        if (clearChatAction.messageRange().isEmpty()) {
            targetChat.removeMessages();
            return;
        }

        clearChatAction.messageRange()
                .stream()
                .map(ActionMessageRangeSync::messages)
                .flatMap(Collection::stream)
                .map(SyncActionMessage::key)
                .filter(Objects::nonNull)
                .forEach(key -> targetChat.removeMessage(entry -> Objects.equals(entry.id(), key.id())));
    }

    private void updateName(Contact contact, Chat chat, ContactAction contactAction) {
        contactAction.fullName().ifPresent(contact::fullName);
        contactAction.firstName().ifPresent(contact::shortName);
        chat.name(contactAction.name());
    }

    private void deleteMessage(MessageInfo message, Chat chat) {
        chat.removeMessage(message);
        socketHandler.onMessageDeleted(message, false);
    }

    private SyncRecord decodePatches(ContactJid jid, BinaryPatchType name, List patches, LTHashState state) {
        var newState = state.copy();
        var results = patches.stream()
                .map(patch -> decodePatch(jid, name, newState, patch))
                .map(MutationsRecord::records)
                .flatMap(Collection::stream)
                .toList();
        return new SyncRecord(newState, results);
    }

    private MutationsRecord decodePatch(ContactJid jid, BinaryPatchType patchType, LTHashState newState, PatchSync patch) {
        if (patch.hasExternalMutations()) {
            Medias.download(patch.externalMutations())
                    .join()
                    .ifPresent(blob -> handleExternalMutation(patch, blob));
        }

        newState.version(patch.encodedVersion());
        var syncMac = calculatePatchMac(jid, patch, patchType);
        Validate.isTrue(!CHECK_MACS || syncMac.isEmpty() || Arrays.equals(syncMac.get(), patch.patchMac()), "sync_mac", HmacValidationException.class);
        var mutations = decodeMutations(jid, patch.mutations(), newState);
        newState.hash(mutations.result().hash());
        newState.indexValueMap(mutations.result().indexValueMap());
        var snapshotMac = calculateSnapshotMac(jid, patchType, newState, patch);
        Validate.isTrue(!CHECK_MACS || snapshotMac.isEmpty() || Arrays.equals(snapshotMac.get(), patch.snapshotMac()), "patch_mac", HmacValidationException.class);
        return mutations;
    }

    private void handleExternalMutation(PatchSync patch, byte[] blob) {
        var mutationsSync = Protobuf.readMessage(blob, MutationsSync.class);
        patch.mutations().addAll(mutationsSync.mutations());
    }

    private Optional calculateSnapshotMac(ContactJid jid, BinaryPatchType name, LTHashState newState, PatchSync patch) {
        return getMutationKeys(jid, patch.keyId())
                .map(mutationKeys -> generateSnapshotMac(newState.hash(), newState.version(), name, mutationKeys.snapshotMacKey()));
    }

    private Optional calculatePatchMac(ContactJid jid, PatchSync patch, BinaryPatchType patchType) {
        return getMutationKeys(jid, patch.keyId())
                .map(mutationKeys -> generatePatchMac(patch.snapshotMac(), getSyncMutationMac(patch), patch.encodedVersion(), patchType, mutationKeys.patchMacKey()));
    }

    private byte[][] getSyncMutationMac(PatchSync patch) {
        return patch.mutations()
                .stream()
                .map(mutation -> mutation.record().value().blob())
                .map(entry -> Arrays.copyOfRange(entry, entry.length - Spec.Signal.KEY_LENGTH, entry.length))
                .toArray(byte[][]::new);
    }

    private Optional decodeSnapshot(ContactJid jid, BinaryPatchType name, SnapshotSync snapshot) {
        var mutationKeys = getMutationKeys(jid, snapshot.keyId());
        if (mutationKeys.isEmpty()) {
            return Optional.empty();
        }
        var newState = new LTHashState(name, snapshot.version().version());
        var mutations = decodeMutations(jid, snapshot.records(), newState);
        newState.hash(mutations.result().hash());
        newState.indexValueMap(mutations.result().indexValueMap());
        Validate.isTrue(!CHECK_MACS || Arrays.equals(snapshot.mac(), generateSnapshotMac(newState.hash(), newState.version(), name, mutationKeys.get()
                .snapshotMacKey())), "decode_snapshot", HmacValidationException.class);
        return Optional.of(new SyncRecord(newState, mutations.records()));
    }

    private Optional getMutationKeys(ContactJid jid, KeyId snapshot) {
        return socketHandler.keys()
                .findAppKeyById(jid, snapshot.id())
                .map(AppStateSyncKey::keyData)
                .map(AppStateSyncKeyData::keyData)
                .map(MutationKeys::of);
    }

    private MutationsRecord decodeMutations(ContactJid jid, List syncs, LTHashState state) {
        var generator = new LTHash(state);
        var mutations = syncs.stream()
                .map(mutation -> decodeMutation(jid, mutation.operation(), mutation.record(), generator))
                .flatMap(Optional::stream)
                .collect(Collectors.toList());
        return new MutationsRecord(generator.finish(), mutations);
    }

    private Optional decodeMutation(ContactJid jid, RecordSync.Operation operation, RecordSync sync, LTHash generator) {
        var mutationKeys = getMutationKeys(jid, sync.keyId());
        if (mutationKeys.isEmpty()) {
            return Optional.empty();
        }
        var blob = sync.value().blob();
        var encryptedBlob = Arrays.copyOfRange(blob, 0, blob.length - Spec.Signal.KEY_LENGTH);
        var encryptedMac = Arrays.copyOfRange(blob, blob.length - Spec.Signal.KEY_LENGTH, blob.length);
        Validate.isTrue(!CHECK_MACS || Arrays.equals(encryptedMac, generateMac(operation, encryptedBlob, sync.keyId().id(), mutationKeys.get().macKey())),
                "decode_mutation", HmacValidationException.class);
        var result = AesCbc.decrypt(encryptedBlob, mutationKeys.get().encKey());
        var actionSync = Protobuf.readMessage(result, ActionDataSync.class);
        Validate.isTrue(!CHECK_MACS || Arrays.equals(sync.index().blob(), Hmac.calculateSha256(actionSync.index(), mutationKeys.get()
                .indexKey())), "decode_mutation", HmacValidationException.class);
        generator.mix(sync.index().blob(), encryptedMac, operation);
        return Optional.of(actionSync);
    }

    private byte[] generateMac(RecordSync.Operation operation, byte[] data, byte[] keyId, byte[] key) {
        var keyData = BytesHelper.concat(operation.content(), keyId);
        var last = new byte[Spec.Signal.MAC_LENGTH];
        last[last.length - 1] = (byte) keyData.length;
        var total = BytesHelper.concat(keyData, data, last);
        var sha512 = Hmac.calculateSha512(total, key);
        return Arrays.copyOfRange(sha512, 0, Spec.Signal.KEY_LENGTH);
    }

    private byte[] generateSnapshotMac(byte[] ltHash, long version, BinaryPatchType patchType, byte[] key) {
        var total = BytesHelper.concat(
                ltHash,
                BytesHelper.longToBytes(version),
                patchType.toString().getBytes(StandardCharsets.UTF_8)
        );
        return Hmac.calculateSha256(total, key);
    }

    private byte[] generatePatchMac(byte[] snapshotMac, byte[][] valueMac, long version, BinaryPatchType patchType, byte[] key) {
        var total = BytesHelper.concat(
                snapshotMac,
                BytesHelper.concat(valueMac),
                BytesHelper.longToBytes(version),
                patchType.toString().getBytes(StandardCharsets.UTF_8)
        );
        return Hmac.calculateSha256(total, key);
    }

    protected void dispose() {
        attempts.clear();
        if(executor != null && !executor.isShutdown()) {
            executor.shutdownNow();
        }
    }

    private record SyncRecord(LTHashState state, List records) {

    }

    private record SnapshotSyncRecord(BinaryPatchType patchType, SnapshotSync snapshot, List patches,
                                      boolean hasMore) {
        public boolean hasSnapshot() {
            return snapshot != null;
        }

        public boolean hasPatches() {
            return patches != null && !patches.isEmpty();
        }
    }

    private record MutationsRecord(LTHash.Result result, List records) {

    }

    private record PatchChunk(BinaryPatchType patchType, List records, boolean hasMore) {

    }

    private record PushRequest(BinaryPatchType type, LTHashState oldState, LTHashState newState, PatchSync sync) {

    }

    public record MutationResult(MutationSync sync, byte[] indexMac, byte[] valueMac, RecordSync.Operation operation) {

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy