<commit_before>// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup';
import { SEALED_SENDER } from '../types/SealedSender';
import * as Errors from '../types/errors';
import type {
  GetProfileOptionsType,
  GetProfileUnauthOptionsType,
} from '../textsecure/WebAPI';
import { HTTPError } from '../textsecure/Errors';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import * as Bytes from '../Bytes';
import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto';
import {
  generateProfileKeyCredentialRequest,
  getClientZkProfileOperations,
  handleProfileKeyCredential,
} from './zkgroup';
import { isMe } from './whatTypeOfConversation';
import type { ConversationModel } from '../models/conversations';
import * as log from '../logging/log';
import { getUserLanguages } from './userLanguages';
import { parseBadgesFromServer } from '../badges/parseBadgesFromServer';
import { strictAssert } from './assert';

async function doGetProfile(c: ConversationModel): Promise<void> {
  const idForLogging = c.idForLogging();
  const { messaging } = window.textsecure;
  strictAssert(
    messaging,
    'getProfile: window.textsecure.messaging not available'
  );

  const { updatesUrl } = window.SignalContext.config;
  strictAssert(
    typeof updatesUrl === 'string',
    'getProfile: expected updatesUrl to be a defined string'
  );

  const clientZkProfileCipher = getClientZkProfileOperations(
    window.getServerPublicParams()
  );

  const userLanguages = getUserLanguages(
    navigator.languages,
    window.getLocale()
  );

  let profile;

  c.deriveAccessKeyIfNeeded();

  const profileKey = c.get('profileKey');
  const profileKeyVersion = c.deriveProfileKeyVersion();
  const uuid = c.getCheckedUuid('getProfile');
  const existingProfileKeyCredential = c.get('profileKeyCredential');
  const lastProfile = c.get('lastProfile');

  let profileCredentialRequestContext:
    | undefined
    | ProfileKeyCredentialRequestContext;

  let getProfileOptions: GetProfileOptionsType | GetProfileUnauthOptionsType;

  let accessKey = c.get('accessKey');
  if (profileKey) {
    strictAssert(
      profileKeyVersion && accessKey,
      'profileKeyVersion and accessKey are derived from profileKey'
    );

    if (existingProfileKeyCredential) {
      getProfileOptions = {
        accessKey,
        profileKeyVersion,
        userLanguages,
      };
    } else {
      log.info(
        'getProfile: generating profile key credential request for ' +
          `conversation ${idForLogging}`
      );

      let profileKeyCredentialRequestHex: undefined | string;
      ({
        requestHex: profileKeyCredentialRequestHex,
        context: profileCredentialRequestContext,
      } = generateProfileKeyCredentialRequest(
        clientZkProfileCipher,
        uuid.toString(),
        profileKey
      ));

      getProfileOptions = {
        accessKey,
        userLanguages,
        profileKeyVersion,
        profileKeyCredentialRequest: profileKeyCredentialRequestHex,
      };
    }
  } else {
    strictAssert(
      !accessKey,
      'accessKey have to be absent because there is no profileKey'
    );

    if (lastProfile?.profileKeyVersion) {
      getProfileOptions = {
        userLanguages,
        profileKeyVersion: lastProfile.profileKeyVersion,
      };
    } else {
      getProfileOptions = { userLanguages };
    }
  }

  const isVersioned = Boolean(getProfileOptions.profileKeyVersion);
  log.info(
    `getProfile: getting ${isVersioned ? 'versioned' : 'unversioned'} ` +
      `profile for conversation ${idForLogging}`
  );

  try {
    if (getProfileOptions.accessKey) {
      try {
        profile = await messaging.getProfile(uuid, getProfileOptions);
      } catch (error) {
        if (!(error instanceof HTTPError)) {
          throw error;
        }
        if (error.code === 401 || error.code === 403) {
          await c.setProfileKey(undefined);

          // Retry fetch using last known profileKeyVersion or fetch
          // unversioned profile.
          return doGetProfile(c);
        }

        if (error.code === 404) {
          await c.removeLastProfile(lastProfile);
        }

        throw error;
      }
    } else {
      try {
        // We won't get the credential, but lets either fetch:
        // - a versioned profile using last known profileKeyVersion
        // - some basic profile information (capabilities, badges, etc).
        profile = await messaging.getProfile(uuid, getProfileOptions);
      } catch (error) {
        if (error instanceof HTTPError && error.code === 404) {
          log.info(`getProfile: failed to find a profile for ${idForLogging}`);

          await c.removeLastProfile(lastProfile);
          if (!isVersioned) {
            log.info(`getProfile: marking ${idForLogging} as unregistered`);
            c.setUnregistered();
          }
        }
        throw error;
      }
    }

    if (profile.identityKey) {
      const identityKey = Bytes.fromBase64(profile.identityKey);
      const changed = await window.textsecure.storage.protocol.saveIdentity(
        new Address(uuid, 1),
        identityKey,
        false
      );
      if (changed) {
        // save identity will close all sessions except for .1, so we
        // must close that one manually.
        const ourUuid = window.textsecure.storage.user.getCheckedUuid();
        await window.textsecure.storage.protocol.archiveSession(
          new QualifiedAddress(ourUuid, new Address(uuid, 1))
        );
      }
    }

    // Update accessKey to prevent race conditions. Since we run asynchronous
    // requests above - it is possible that someone updates or erases
    // the profile key from under us.
    accessKey = c.get('accessKey');

    if (profile.unrestrictedUnidentifiedAccess && profile.unidentifiedAccess) {
      log.info(
        `getProfile: setting sealedSender to UNRESTRICTED for conversation ${idForLogging}`
      );
      c.set({
        sealedSender: SEALED_SENDER.UNRESTRICTED,
      });
    } else if (accessKey && profile.unidentifiedAccess) {
      const haveCorrectKey = verifyAccessKey(
        Bytes.fromBase64(accessKey),
        Bytes.fromBase64(profile.unidentifiedAccess)
      );

      if (haveCorrectKey) {
        log.info(
          `getProfile: setting sealedSender to ENABLED for conversation ${idForLogging}`
        );
        c.set({
          sealedSender: SEALED_SENDER.ENABLED,
        });
      } else {
        log.warn(
          `getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}`
        );
        c.set({
          sealedSender: SEALED_SENDER.DISABLED,
        });
      }
    } else {
      log.info(
        `getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}`
      );
      c.set({
        sealedSender: SEALED_SENDER.DISABLED,
      });
    }

    const rawDecryptionKey = c.get('profileKey') || lastProfile?.profileKey;
    const decryptionKey = rawDecryptionKey
      ? Bytes.fromBase64(rawDecryptionKey)
      : undefined;
    if (profile.about) {
      if (decryptionKey) {
        const decrypted = decryptProfile(
          Bytes.fromBase64(profile.about),
          decryptionKey
        );
        c.set('about', Bytes.toString(trimForDisplay(decrypted)));
      }
    } else {
      c.unset('about');
    }

    if (profile.aboutEmoji) {
      if (decryptionKey) {
        const decrypted = decryptProfile(
          Bytes.fromBase64(profile.aboutEmoji),
          decryptionKey
        );
        c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted)));
      }
    } else {
      c.unset('aboutEmoji');
    }

    if (profile.paymentAddress && isMe(c.attributes)) {
      window.storage.put('paymentAddress', profile.paymentAddress);
    }

    if (profile.capabilities) {
      c.set({ capabilities: profile.capabilities });
    } else {
      c.unset('capabilities');
    }

    const badges = parseBadgesFromServer(profile.badges, updatesUrl);
    if (badges.length) {
      await window.reduxActions.badges.updateOrCreate(badges);
      c.set({
        badges: badges.map(badge => ({
          id: badge.id,
          ...('expiresAt' in badge
            ? {
                expiresAt: badge.expiresAt,
                isVisible: badge.isVisible,
              }
            : {}),
        })),
      });
    } else {
      c.unset('badges');
    }

    if (profileCredentialRequestContext) {
      if (profile.credential) {
        const profileKeyCredential = handleProfileKeyCredential(
          clientZkProfileCipher,
          profileCredentialRequestContext,
          profile.credential
        );
        c.set({ profileKeyCredential });
      } else {
        c.unset('profileKeyCredential');
      }
    }
  } catch (error) {
    if (!(error instanceof HTTPError)) {
      throw error;
    }

    switch (error.code) {
      case 401:
      case 403:
        if (
          c.get('sealedSender') === SEALED_SENDER.ENABLED ||
          c.get('sealedSender') === SEALED_SENDER.UNRESTRICTED
        ) {
          log.warn(
            `getProfile: Got 401/403 when using accessKey for ${idForLogging}, removing profileKey`
          );
          c.setProfileKey(undefined);
        }
        if (c.get('sealedSender') === SEALED_SENDER.UNKNOWN) {
          log.warn(
            `getProfile: Got 401/403 when using accessKey for ${idForLogging}, setting sealedSender = DISABLED`
          );
          c.set('sealedSender', SEALED_SENDER.DISABLED);
        }
        return;
      default:
        log.warn(
          'getProfile failure:',
          idForLogging,
          Errors.toLogFormat(error)
        );
        return;
    }
  }

  const decryptionKeyString = profileKey || lastProfile?.profileKey;
  const decryptionKey = decryptionKeyString
    ? Bytes.fromBase64(decryptionKeyString)
    : undefined;

  let isSuccessfullyDecrypted = true;
  if (profile.name) {
    if (decryptionKey) {
      try {
        await c.setEncryptedProfileName(profile.name, decryptionKey);
      } catch (error) {
        log.warn(
          'getProfile decryption failure:',
          idForLogging,
          Errors.toLogFormat(error)
        );
        isSuccessfullyDecrypted = false;
        await c.set({
          profileName: undefined,
          profileFamilyName: undefined,
        });
      }
    }
  } else {
    c.set({
      profileName: undefined,
      profileFamilyName: undefined,
    });
  }

  try {
    if (decryptionKey) {
      await c.setProfileAvatar(profile.avatar, decryptionKey);
    }
  } catch (error) {
    if (error instanceof HTTPError) {
      if (error.code === 403 || error.code === 404) {
        log.warn(
          `getProfile: clearing profile avatar for conversation ${idForLogging}`
        );
        c.set({
          profileAvatar: null,
        });
      }
    } else {
      log.warn(<commit_msg>Profile fetches shouldn't drop our own profile key<commit_after>// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup';
import { SEALED_SENDER } from '../types/SealedSender';
import * as Errors from '../types/errors';
import type {
  GetProfileOptionsType,
  GetProfileUnauthOptionsType,
} from '../textsecure/WebAPI';
import { HTTPError } from '../textsecure/Errors';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import * as Bytes from '../Bytes';
import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto';
import {
  generateProfileKeyCredentialRequest,
  getClientZkProfileOperations,
  handleProfileKeyCredential,
} from './zkgroup';
import { isMe } from './whatTypeOfConversation';
import type { ConversationModel } from '../models/conversations';
import * as log from '../logging/log';
import { getUserLanguages } from './userLanguages';
import { parseBadgesFromServer } from '../badges/parseBadgesFromServer';
import { strictAssert } from './assert';

async function doGetProfile(c: ConversationModel): Promise<void> {
  const idForLogging = c.idForLogging();
  const { messaging } = window.textsecure;
  strictAssert(
    messaging,
    'getProfile: window.textsecure.messaging not available'
  );

  const { updatesUrl } = window.SignalContext.config;
  strictAssert(
    typeof updatesUrl === 'string',
    'getProfile: expected updatesUrl to be a defined string'
  );

  const clientZkProfileCipher = getClientZkProfileOperations(
    window.getServerPublicParams()
  );

  const userLanguages = getUserLanguages(
    navigator.languages,
    window.getLocale()
  );

  let profile;

  c.deriveAccessKeyIfNeeded();

  const profileKey = c.get('profileKey');
  const profileKeyVersion = c.deriveProfileKeyVersion();
  const uuid = c.getCheckedUuid('getProfile');
  const existingProfileKeyCredential = c.get('profileKeyCredential');
  const lastProfile = c.get('lastProfile');

  let profileCredentialRequestContext:
    | undefined
    | ProfileKeyCredentialRequestContext;

  let getProfileOptions: GetProfileOptionsType | GetProfileUnauthOptionsType;

  let accessKey = c.get('accessKey');
  if (profileKey) {
    strictAssert(
      profileKeyVersion && accessKey,
      'profileKeyVersion and accessKey are derived from profileKey'
    );

    if (existingProfileKeyCredential) {
      getProfileOptions = {
        accessKey,
        profileKeyVersion,
        userLanguages,
      };
    } else {
      log.info(
        'getProfile: generating profile key credential request for ' +
          `conversation ${idForLogging}`
      );

      let profileKeyCredentialRequestHex: undefined | string;
      ({
        requestHex: profileKeyCredentialRequestHex,
        context: profileCredentialRequestContext,
      } = generateProfileKeyCredentialRequest(
        clientZkProfileCipher,
        uuid.toString(),
        profileKey
      ));

      getProfileOptions = {
        accessKey,
        userLanguages,
        profileKeyVersion,
        profileKeyCredentialRequest: profileKeyCredentialRequestHex,
      };
    }
  } else {
    strictAssert(
      !accessKey,
      'accessKey have to be absent because there is no profileKey'
    );

    if (lastProfile?.profileKeyVersion) {
      getProfileOptions = {
        userLanguages,
        profileKeyVersion: lastProfile.profileKeyVersion,
      };
    } else {
      getProfileOptions = { userLanguages };
    }
  }

  const isVersioned = Boolean(getProfileOptions.profileKeyVersion);
  log.info(
    `getProfile: getting ${isVersioned ? 'versioned' : 'unversioned'} ` +
      `profile for conversation ${idForLogging}`
  );

  try {
    if (getProfileOptions.accessKey) {
      try {
        profile = await messaging.getProfile(uuid, getProfileOptions);
      } catch (error) {
        if (!(error instanceof HTTPError)) {
          throw error;
        }
        if (error.code === 401 || error.code === 403) {
          if (!isMe(c.attributes)) {
            await c.setProfileKey(undefined);
          }

          // Retry fetch using last known profileKeyVersion or fetch
          // unversioned profile.
          return doGetProfile(c);
        }

        if (error.code === 404) {
          await c.removeLastProfile(lastProfile);
        }

        throw error;
      }
    } else {
      try {
        // We won't get the credential, but lets either fetch:
        // - a versioned profile using last known profileKeyVersion
        // - some basic profile information (capabilities, badges, etc).
        profile = await messaging.getProfile(uuid, getProfileOptions);
      } catch (error) {
        if (error instanceof HTTPError && error.code === 404) {
          log.info(`getProfile: failed to find a profile for ${idForLogging}`);

          await c.removeLastProfile(lastProfile);
          if (!isVersioned) {
            log.info(`getProfile: marking ${idForLogging} as unregistered`);
            c.setUnregistered();
          }
        }
        throw error;
      }
    }

    if (profile.identityKey) {
      const identityKey = Bytes.fromBase64(profile.identityKey);
      const changed = await window.textsecure.storage.protocol.saveIdentity(
        new Address(uuid, 1),
        identityKey,
        false
      );
      if (changed) {
        // save identity will close all sessions except for .1, so we
        // must close that one manually.
        const ourUuid = window.textsecure.storage.user.getCheckedUuid();
        await window.textsecure.storage.protocol.archiveSession(
          new QualifiedAddress(ourUuid, new Address(uuid, 1))
        );
      }
    }

    // Update accessKey to prevent race conditions. Since we run asynchronous
    // requests above - it is possible that someone updates or erases
    // the profile key from under us.
    accessKey = c.get('accessKey');

    if (profile.unrestrictedUnidentifiedAccess && profile.unidentifiedAccess) {
      log.info(
        `getProfile: setting sealedSender to UNRESTRICTED for conversation ${idForLogging}`
      );
      c.set({
        sealedSender: SEALED_SENDER.UNRESTRICTED,
      });
    } else if (accessKey && profile.unidentifiedAccess) {
      const haveCorrectKey = verifyAccessKey(
        Bytes.fromBase64(accessKey),
        Bytes.fromBase64(profile.unidentifiedAccess)
      );

      if (haveCorrectKey) {
        log.info(
          `getProfile: setting sealedSender to ENABLED for conversation ${idForLogging}`
        );
        c.set({
          sealedSender: SEALED_SENDER.ENABLED,
        });
      } else {
        log.warn(
          `getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}`
        );
        c.set({
          sealedSender: SEALED_SENDER.DISABLED,
        });
      }
    } else {
      log.info(
        `getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}`
      );
      c.set({
        sealedSender: SEALED_SENDER.DISABLED,
      });
    }

    const rawDecryptionKey = c.get('profileKey') || lastProfile?.profileKey;
    const decryptionKey = rawDecryptionKey
      ? Bytes.fromBase64(rawDecryptionKey)
      : undefined;
    if (profile.about) {
      if (decryptionKey) {
        const decrypted = decryptProfile(
          Bytes.fromBase64(profile.about),
          decryptionKey
        );
        c.set('about', Bytes.toString(trimForDisplay(decrypted)));
      }
    } else {
      c.unset('about');
    }

    if (profile.aboutEmoji) {
      if (decryptionKey) {
        const decrypted = decryptProfile(
          Bytes.fromBase64(profile.aboutEmoji),
          decryptionKey
        );
        c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted)));
      }
    } else {
      c.unset('aboutEmoji');
    }

    if (profile.paymentAddress && isMe(c.attributes)) {
      window.storage.put('paymentAddress', profile.paymentAddress);
    }

    if (profile.capabilities) {
      c.set({ capabilities: profile.capabilities });
    } else {
      c.unset('capabilities');
    }

    const badges = parseBadgesFromServer(profile.badges, updatesUrl);
    if (badges.length) {
      await window.reduxActions.badges.updateOrCreate(badges);
      c.set({
        badges: badges.map(badge => ({
          id: badge.id,
          ...('expiresAt' in badge
            ? {
                expiresAt: badge.expiresAt,
                isVisible: badge.isVisible,
              }
            : {}),
        })),
      });
    } else {
      c.unset('badges');
    }

    if (profileCredentialRequestContext) {
      if (profile.credential) {
        const profileKeyCredential = handleProfileKeyCredential(
          clientZkProfileCipher,
          profileCredentialRequestContext,
          profile.credential
        );
        c.set({ profileKeyCredential });
      } else {
        c.unset('profileKeyCredential');
      }
    }
  } catch (error) {
    if (!(error instanceof HTTPError)) {
      throw error;
    }

    switch (error.code) {
      case 401:
      case 403:
        if (
          c.get('sealedSender') === SEALED_SENDER.ENABLED ||
          c.get('sealedSender') === SEALED_SENDER.UNRESTRICTED
        ) {
          log.warn(
            `getProfile: Got 401/403 when using accessKey for ${idForLogging}, removing profileKey`
          );
          if (!isMe(c.attributes)) {
            await c.setProfileKey(undefined);
          }
        }
        if (c.get('sealedSender') === SEALED_SENDER.UNKNOWN) {
          log.warn(
            `getProfile: Got 401/403 when using accessKey for ${idForLogging}, setting sealedSender = DISABLED`
          );
          c.set('sealedSender', SEALED_SENDER.DISABLED);
        }
        return;
      default:
        log.warn(
          'getProfile failure:',
          idForLogging,
          Errors.toLogFormat(error)
        );
        return;
    }
  }

  const decryptionKeyString = profileKey || lastProfile?.profileKey;
  const decryptionKey = decryptionKeyString
    ? Bytes.fromBase64(decryptionKeyString)
    : undefined;

  let isSuccessfullyDecrypted = true;
  if (profile.name) {
    if (decryptionKey) {
      try {
        await c.setEncryptedProfileName(profile.name, decryptionKey);
      } catch (error) {
        log.warn(
          'getProfile decryption failure:',
          idForLogging,
          Errors.toLogFormat(error)
        );
        isSuccessfullyDecrypted = false;
        await c.set({
          profileName: undefined,
          profileFamilyName: undefined,
        });
      }
    }
  } else {
    c.set({
      profileName: undefined,
      profileFamilyName: undefined,
    });
  }

  try {
    if (decryptionKey) {
      await c.setProfileAvatar(profile.avatar, decryptionKey);
    }
  } catch (error) {
    if (error instanceof HTTPError) {
      if (error.code === 403 || error.code === 404) {
        log.warn(
          `getProfile: profile avatar is missing for conversation ${idForLogging}`
        );
      }
    } else {
      log.warn(
{'fraction_non_alphanumeric': 0.10632443161313605, 'fraction_numerical': 0.005683868639480332, 'mean_word_length': 2.417450285185756, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5357549', 'n_tokens_mistral': 6436, 'n_tokens_neox': 6296, 'n_words': 1367}