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

de.thoffbauer.signal4j.SignalService Maven / Gradle / Ivy

The newest version!
package de.thoffbauer.signal4j;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceAccountManager.NewDeviceRegistrationReturn;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.Request;

import de.thoffbauer.signal4j.exceptions.NoGroupFoundException;
import de.thoffbauer.signal4j.listener.ConversationListener;
import de.thoffbauer.signal4j.listener.SecurityExceptionListener;
import de.thoffbauer.signal4j.store.DataStore;
import de.thoffbauer.signal4j.store.Group;
import de.thoffbauer.signal4j.store.GroupId;
import de.thoffbauer.signal4j.store.JsonSignalStore;
import de.thoffbauer.signal4j.store.SignalStore;
import de.thoffbauer.signal4j.store.User;
import de.thoffbauer.signal4j.store.WhisperTrustStore;
import de.thoffbauer.signal4j.util.Base64;
import de.thoffbauer.signal4j.util.SecretUtil;

public class SignalService {
	
	/**
	 * Path to main store file. Contains all keys etc.
	 */
	public static String STORE_PATH = "store.json";
	/**
	 * Folder to save attachments to
	 */
	public static String ATTACHMENTS_PATH = "attachments";
	
	private static final int PASSWORD_LENGTH = 18;
	private static final int SIGNALING_KEY_LENGTH = 52;
	private static final int MAX_REGISTRATION_ID = 8192;
	private static final int PREKEYS_BATCH_SIZE = 100;
	private static final int MAX_PREKEY_ID = Medium.MAX_VALUE;
	
	private final TrustStore trustStore = new WhisperTrustStore();
	
	private SignalServiceAccountManager accountManager;
	private SignalServiceMessageSender messageSender;
	private SignalServiceMessagePipe messagePipe;
	private SignalServiceMessageReceiver messageReceiver;
	private SignalServiceCipher cipher;
	private SignalStore store;
	private IdentityKeyPair tempIdentity;
	
	private ArrayList conversationListeners = new ArrayList<>();
	private ArrayList securityExceptionListeners = new ArrayList<>();
	
	/**
	 * Create a new instance. Will automatically load a store file if existent.
	 * @throws IOException can be thrown while loading the store
	 */
	public SignalService() throws IOException {
		// Add bouncycastle
		Security.insertProviderAt(new org.bouncycastle.jce.provider.BouncyCastleProvider(), 1);
		
		File storeFile = new File(STORE_PATH);
		if(storeFile.isFile()) {
			store = JsonSignalStore.load(storeFile);
			accountManager = new SignalServiceAccountManager(store.getUrl(), trustStore, store.getPhoneNumber(),
					store.getPassword(), store.getDeviceId(), store.getUserAgent());
		} else {
			store = new JsonSignalStore();
		}
	}
	
	/**
	 * Starts the connection and registration as the primary device. This creates a new Signal account with this number.
	 * @param url the url of the signal server
	 * @param userAgent human-readable name of the user agent
	 * @param phoneNumber the user's phone number
	 * @param voice whether to call (true) or to message (false) for verification
	 * @throws IOException
	 */
	public void startConnectAsPrimary(String url, String userAgent, String phoneNumber, boolean voice) throws IOException {
		if(accountManager != null) {
			throw new IllegalStateException("Already started a connection!");
		}
		store.setUrl(url);
		store.setUserAgent(userAgent);
		store.setPhoneNumber(phoneNumber);
		createPasswords();
		store.setDeviceId(SignalServiceAddress.DEFAULT_DEVICE_ID);
		accountManager = new SignalServiceAccountManager(url, trustStore, phoneNumber, 
				store.getPassword(), userAgent);
		if(voice) {
			accountManager.requestVoiceVerificationCode();
		} else {
			accountManager.requestSmsVerificationCode();
		}
	}
	
	/**
	 * Finish the connection and registration as primary device with the received verification code
	 * @param verificationCode the verification code without the -
	 * @throws IOException
	 */
	public void finishConnectAsPrimary(String verificationCode) throws IOException {
		if(accountManager == null) {
			throw new IllegalStateException("Cannot finish: No connection started!");
		} else if(isRegistered()) {
			throw new IllegalStateException("Already registered!");
		}
		createRegistrationId();
		accountManager.verifyAccountWithCode(verificationCode, store.getSignalingKey(), 
				store.getLocalRegistrationId(), false, true);
		IdentityKeyPair identityKeyPair = KeyHelper.generateIdentityKeyPair();
		store.setIdentityKeyPair(identityKeyPair);
		store.setLastResortPreKey(KeyHelper.generateLastResortPreKey());
		checkPreKeys(-1);
		save();
	}
	
	/**
	 * Start connection and registration as secondary device. The device will be linked with the device scanning accepting the code.
	 * @param url the url of the signal server
	 * @param userAgent human-readable name of the user agent
	 * @param phoneNumber the user's phone number
	 * @return a url which must be shown as a QR code to the android app for provisioning
	 * @throws IOException
	 * @throws TimeoutException
	 */
	public String startConnectAsSecondary(String url, String userAgent, String phoneNumber) throws IOException, TimeoutException {
		if(accountManager != null) {
			throw new IllegalStateException("Already started a connection!");
		}
		store.setUrl(url);
		store.setUserAgent(userAgent);
		store.setPhoneNumber(phoneNumber);
		createPasswords();
		createRegistrationId();
		accountManager = new SignalServiceAccountManager(url, trustStore, phoneNumber, 
				store.getPassword(), userAgent);
		String uuid = accountManager.getNewDeviceUuid();
		
		tempIdentity = KeyHelper.generateIdentityKeyPair();
		byte[] publicKeyBytes = tempIdentity.getPublicKey().serialize();
		String publicKeyBase64 = Base64.encodeBytesWithoutPadding(publicKeyBytes);

		String qrString = "tsdevice:/?uuid=" + URLEncoder.encode(uuid, "UTF-8") + 
				"&pub_key=" + URLEncoder.encode(publicKeyBase64, "UTF-8");
		return qrString;
	}
	
	/**
	 * Blocking call. Call this directly after {@code startConnectAsSecondary()} and this method will wait
	 * for the master device accepting this device.
	 * @param deviceName a name for this device (not the user agent)
	 * @param supportsSms whether this device can receive and send SMS
	 * @throws IOException
	 * @throws TimeoutException
	 */
	public void finishConnectAsSecondary(String deviceName, boolean supportsSms) throws IOException, TimeoutException {
		if(accountManager == null) {
			throw new IllegalStateException("Cannot finish: No connection started!");
		} else if(isRegistered()) {
			throw new IllegalStateException("Already registered!");
		}
		try {
			NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(tempIdentity,
					store.getSignalingKey(), supportsSms, true, store.getLocalRegistrationId(), deviceName);
			store.setDeviceId(ret.getDeviceId());
			store.setIdentityKeyPair(ret.getIdentity());
		} catch (InvalidKeyException e) {
			throw new RuntimeException("This can not happen - theoretically", e);
		}
		store.setLastResortPreKey(KeyHelper.generateLastResortPreKey());
		checkPreKeys(-1);
		save();
	}

	/**
	 * Send a data, i.e. "normal" message
	 * @param address
	 * @param message
	 * @throws IOException
	 */
	public void sendMessage(String address, SignalServiceDataMessage message) throws IOException {
		checkRegistered();
		checkMessageSender();
		try {
			messageSender.sendMessage(new SignalServiceAddress(address), message);
		} catch (UntrustedIdentityException e) {
			fireSecurityException(new SignalServiceAddress(address), e);
		}
		save();
	}
	
	/**
	 * Send a data, i.e. "normal" message to a group
	 * @param addresses
	 * @param message
	 * @throws IOException
	 */
	public void sendMessage(List addresses, SignalServiceDataMessage message) throws IOException {
		checkRegistered();
		checkMessageSender();
		List signalServiceAddresses = addresses.stream()
				.filter(v -> !v.equals(store.getPhoneNumber()))
				.map(v -> new SignalServiceAddress(v))
				.collect(Collectors.toList());
		try {
			messageSender.sendMessage(signalServiceAddresses, message);
		} catch (EncapsulatedExceptions e) {
			for(UntrustedIdentityException ex : e.getUntrustedIdentityExceptions()) {
				fireSecurityException(new SignalServiceAddress(ex.getE164Number()), ex);
			}
			for(UnregisteredUserException ex : e.getUnregisteredUserExceptions()) {
				fireSecurityException(new SignalServiceAddress(ex.getE164Number()), ex);
			}
			if(!e.getNetworkExceptions().isEmpty()) {
				throw new IOException(e.getNetworkExceptions().size() + " network exception(s)! One is following.", 
						e.getNetworkExceptions().get(0));
			}
		}
		save();
	}
	
	/**
	 * Notify other devices that these messages have been read.
	 * @param messages
	 * @throws IOException
	 */
	public void markRead(List messages) throws IOException {
		checkRegistered();
		checkMessageSender();
		try {
			SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forRead(messages);
			messageSender.sendMessage(syncMessage);
		} catch (UntrustedIdentityException e) {
			fireSecurityException(new SignalServiceAddress(store.getPhoneNumber()), e);
		}
	}
	
	/**
	 * Request sync messages from primary device. They are received using the listeners
	 * @throws IOException
	 * @throws UntrustedIdentityException
	 */
	public void requestSync() throws IOException {
		try {
			checkRegistered();
			checkMessageSender();
			Request.Type[] types = new Request.Type[] {Request.Type.CONTACTS, Request.Type.GROUPS, Request.Type.BLOCKED};
			for(Request.Type type : types) {
				RequestMessage request = new RequestMessage(Request.newBuilder().setType(type).build());
				SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forRequest(request);
				messageSender.sendMessage(syncMessage);
			}
		} catch(UntrustedIdentityException e) {
			fireSecurityException(new SignalServiceAddress(store.getPhoneNumber()), e);
		}
	}

	private void checkMessageSender() {
		if(messageSender == null) {
			messageSender = new SignalServiceMessageSender(store.getUrl(), trustStore, 
					store.getPhoneNumber(), store.getPassword(), store.getDeviceId(), store, 
					store.getUserAgent(), Optional.absent());
		}
	}
	
	private void checkRegistered() {
		if(!isRegistered()) {
			throw new IllegalStateException("Not registered!");
		}
	}

	/**
	 * Returns true if this device is registered. This does not necessarily 
	 * mean that no other device has registered with this number.
	 * @return whether this device is registered
	 */
	public boolean isRegistered() {
		return store.getIdentityKeyPair() != null;
	}
	
	private void createPasswords() {
		String password = SecretUtil.getSecret(PASSWORD_LENGTH);
		store.setPassword(password);
		String signalingKey= SecretUtil.getSecret(SIGNALING_KEY_LENGTH);
		store.setSignalingKey(signalingKey);
	}
	
	private void createRegistrationId() {
		int registrationId = new Random().nextInt(MAX_REGISTRATION_ID);
		store.setLocalRegistrationId(registrationId);
	}
	
	/**
	 * Saves the store. As this is done automatically inside the library, 
	 * you only need to call this if you change sometihng manually.
	 * @throws IOException
	 */
	public void save() throws IOException {
		store.save(new File(STORE_PATH));
	}

	public void addConversationListener(ConversationListener listener) {
		conversationListeners.add(listener);
	}
	
	public void removeConversationListener(ConversationListener listener) {
		conversationListeners.remove(listener);
	}
	
	/**
	 * Add a listener for exceptions regarding the security of communication.
	 * @param listener
	 */
	public void addSecurityExceptionListener(SecurityExceptionListener listener) {
		securityExceptionListeners.add(listener);
	}
	
	/**
	 * Remove a security exception listener
	 * @param listener
	 */
	public void removeSecurityExceptionListener(SecurityExceptionListener listener) {
		securityExceptionListeners.remove(listener);
	}
	
	/**
	 * Wait for incoming messages. This method returns silently if the timeout passes.
	 * If a message arrives, the conversation listeners are called and the method returns.
	 * @param timeoutMillis time to wait for messages
	 * @throws IOException
	 */
	public void pull(int timeoutMillis) throws IOException {
		checkRegistered();
		if(messagePipe == null) {
			messageReceiver = new SignalServiceMessageReceiver(store.getUrl(), 
					trustStore, store.getPhoneNumber(), store.getPassword(), store.getDeviceId(), 
					store.getSignalingKey(), store.getUserAgent());
			messagePipe = messageReceiver.createMessagePipe();
		}
		SignalServiceEnvelope envelope = null;
		try {
			try {
				envelope = messagePipe.read(timeoutMillis, TimeUnit.MILLISECONDS);
			} catch (TimeoutException e) {
				return;
			}
			if(!envelope.isReceipt() && (envelope.hasContent() || envelope.hasLegacyMessage())) {
				if(cipher == null) {
					cipher = new SignalServiceCipher(new SignalServiceAddress(store.getPhoneNumber()), store);
				}
				SignalServiceContent content = cipher.decrypt(envelope);
				if(content.getDataMessage().isPresent()) {
					SignalServiceDataMessage dataMessage = content.getDataMessage().get();
					handleDataMessage(envelope, dataMessage);
				} else if(content.getSyncMessage().isPresent()) {
					SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
					handleSyncMessage(envelope, syncMessage);
				}
			}
			save();
		} catch (InvalidVersionException | InvalidMessageException | InvalidKeyException | DuplicateMessageException | InvalidKeyIdException | org.whispersystems.libsignal.UntrustedIdentityException | LegacyMessageException e) {
			fireSecurityException(envelope != null ? envelope.getSourceAddress() : null, e);
		} catch(NoSessionException e) {
			throw new RuntimeException("The store file seems to be corrupt!", e);
		}
	}

	private void handleDataMessage(SignalServiceEnvelope envelope, SignalServiceDataMessage dataMessage) throws IOException {
		if(dataMessage.getGroupInfo().isPresent()) {
			SignalServiceGroup groupInfo = dataMessage.getGroupInfo().get();
			GroupId id = new GroupId(groupInfo.getGroupId());
			Group group = store.getDataStore().getGroup(id);
			if(groupInfo.getType() == SignalServiceGroup.Type.UPDATE) {
				if(group == null) {
					group = new Group(id);
					group.setActive(true);
					store.getDataStore().addGroup(group);
				}
				if(groupInfo.getName().isPresent()) {
					group.setName(groupInfo.getName().get());
				}
				if(groupInfo.getMembers().isPresent()) {
					group.setMembers(new ArrayList<>(groupInfo.getMembers().get()));
				}
				if(groupInfo.getAvatar().isPresent()) {
					SignalServiceAttachment attachment = groupInfo.getAvatar().get();
					String avatarId = UUID.randomUUID().toString();
					saveAttachment(toUser(envelope.getSourceAddress()), attachment, null, avatarId);
					if(group.getAvatarId() != null) {
						deleteAttachment(group.getAvatarId());
					}
					group.setAvatarId(avatarId);
				}
				fireGroupUpdate(envelope.getSourceAddress(), group);
			} else if(groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
				if(group != null) {
					if(envelope.getSourceAddress().getNumber().equals(store.getPhoneNumber())) {
						group.setActive(false);
					}
					group.getMembers().remove(envelope.getSourceAddress().getNumber());
					fireGroupUpdate(envelope.getSourceAddress(), group);
				}
			} else {
				if(group == null) {
					fireSecurityException(envelope.getSourceAddress(), new NoGroupFoundException("No group known for ID", id));
				}
				fireMessage(envelope.getSourceAddress(), dataMessage, group);
			}
		} else {
			fireMessage(envelope.getSourceAddress(), dataMessage, null);
		}
	}

	private void handleSyncMessage(SignalServiceEnvelope envelope, SignalServiceSyncMessage syncMessage)
			throws IOException, FileNotFoundException {
		if(syncMessage.getContacts().isPresent()) {
			File file = saveAttachment(envelope.getSourceAddress(), syncMessage.getContacts().get(), null);
			DeviceContactsInputStream in = new DeviceContactsInputStream(new FileInputStream(file));
			ArrayList contacts = new ArrayList<>();
			while(true) {
				DeviceContact deviceContact = in.read();
				if(deviceContact == null) {
					//EOF
					break;
				}
				User contact = new User(deviceContact);
				contacts.add(contact);
				if(deviceContact.getAvatar().isPresent()) {
					String id = UUID.randomUUID().toString();
					saveAttachment(toUser(envelope.getSourceAddress()), deviceContact.getAvatar().get(), 
							null, id);
					contact.setAvatarId(id);
				}
			}
			file.delete();
			store.getDataStore().getContacts().stream()
					.filter(v -> v.getAvatarId() != null)
					.forEach(v -> deleteAttachment(v.getAvatarId()));
			store.getDataStore().overwriteContacts(contacts);
			for(User contact : contacts) {
				fireContactUpdate(contact);
			}
		} else if(syncMessage.getGroups().isPresent()) {
			File file = saveAttachment(envelope.getSourceAddress(), syncMessage.getGroups().get(), null);
			DeviceGroupsInputStream in = new DeviceGroupsInputStream(new FileInputStream(file));
			List groups = new ArrayList<>();
			while(true) {
				DeviceGroup deviceGroup = in.read();
				if(deviceGroup == null) {
					//EOF
					break;
				}
				Group group = new Group(deviceGroup);
				groups.add(group);
				if(deviceGroup.getAvatar().isPresent()) {
					String id = UUID.randomUUID().toString();
					saveAttachment(toUser(envelope.getSourceAddress()), deviceGroup.getAvatar().get(), 
							null, id);
					group.setAvatarId(id);
				}
			}
			file.delete();
			store.getDataStore().getGroups().stream()
					.filter(v -> v.getAvatarId() != null)
					.forEach(v -> deleteAttachment(v.getAvatarId()));
			store.getDataStore().overwriteGroups(groups);
			for(Group group : groups) {
				fireGroupUpdate(envelope.getSourceAddress(), group);
			}
		} else if(syncMessage.getBlockedList().isPresent()) {
			BlockedListMessage blockedMessage = syncMessage.getBlockedList().get();
			List blocked = blockedMessage.getNumbers();
			for(User contact : store.getDataStore().getContacts()) {
				boolean isNewBlocked = blocked.contains(contact.getNumber());
				if(contact.isBlocked() && !isNewBlocked) {
					contact.setBlocked(false);
					fireContactUpdate(contact);
				} else if(!contact.isBlocked() && isNewBlocked) {
					contact.setBlocked(true);
					fireContactUpdate(contact);
				}
			}
		} else if(syncMessage.getRead().isPresent()) {
			List reads = syncMessage.getRead().get();
			fireReadUpdate(reads);
		} else if(syncMessage.getSent().isPresent()) {
			SentTranscriptMessage transcript = syncMessage.getSent().get();
			handleDataMessage(envelope, transcript.getMessage());
		}
	}
	
	private void fireContactUpdate(User contact) throws IOException {
		for(ConversationListener listener : conversationListeners) {
			listener.onContactUpdate(contact);
		}
	}

	private void fireMessage(SignalServiceAddress address, SignalServiceDataMessage dataMessage, Group group) {
		for(ConversationListener listener : conversationListeners) {
			listener.onMessage(toUser(address), dataMessage, group);
		}
	}

	private void fireGroupUpdate(SignalServiceAddress address, Group group) throws IOException {
		for(ConversationListener listener : conversationListeners) {
			listener.onGroupUpdate(toUser(address), group);
		}
	}
	
	private void fireReadUpdate(List readList) {
		for(ConversationListener listener : conversationListeners) {
			listener.onReadUpdate(readList);
		}
	}
	
	private void fireSecurityException(SignalServiceAddress sender, Exception e) {
		fireSecurityException(toUser(sender), e);
	}
	
	private void fireSecurityException(User sender, Exception e) {
		for(SecurityExceptionListener listener : securityExceptionListeners) {
			listener.onSecurityException(sender, e);
		}
	}
	
	/**
	 * Tries to find the address in the stored contacts and creates new user if necessary (but does not store it).
	 * @param address
	 * @return the found contact or the new user
	 */
	public User toUser(SignalServiceAddress address) {
		if(address == null) {
			return null;
		}
		User user = store.getDataStore().getContact(address.getNumber());
		if(user == null) {
			user = new User(address.getNumber());
		}
		return user;
	}
	
	/**
	 * Ensures that there are enough prekeys available. Has to be called regularly.
* Every time somebody sends you a message, he uses one of your prekeys which you have uploaded earlier. * To always have one prekey available, you also upload a last resort key. You should always * have enough prekeys to prevent key reusing. * @param minimumKeys the minimum amount of keys to register. Must be below 100. * @throws IOException */ public void checkPreKeys(int minimumKeys) throws IOException { if(minimumKeys > PREKEYS_BATCH_SIZE) { throw new IllegalArgumentException("PreKeys count must be below or equal to " + PREKEYS_BATCH_SIZE); } checkRegistered(); int preKeysCount = accountManager.getPreKeysCount(); if(preKeysCount < minimumKeys || minimumKeys < 0) { try { // generate prekeys int nextPreKeyId = store.getNextPreKeyId(); ArrayList preKeys = new ArrayList<>(); for(int i = 0; i < PREKEYS_BATCH_SIZE; i++) { PreKeyRecord record = new PreKeyRecord(nextPreKeyId, Curve.generateKeyPair()); store.storePreKey(record.getId(), record); preKeys.add(record); nextPreKeyId = (nextPreKeyId + 1) % MAX_PREKEY_ID; } store.setNextPreKeyId(nextPreKeyId); // generate signed prekey int nextSignedPreKeyId = store.getNextSignedPreKeyId(); SignedPreKeyRecord signedPreKey = KeyHelper.generateSignedPreKey(store.getIdentityKeyPair(), nextSignedPreKeyId); store.storeSignedPreKey(signedPreKey.getId(), signedPreKey); store.setNextSignedPreKeyId((nextSignedPreKeyId + 1) % MAX_PREKEY_ID); // upload accountManager.setPreKeys(store.getIdentityKeyPair().getPublicKey(), store.getLastResortPreKey(), signedPreKey, preKeys); } catch (InvalidKeyException e) { throw new RuntimeException("Stored identity corrupt!", e); } save(); } } /** * Save an attachment to the attachments folder specified by {@code ATTACHMENTS_PATH}. * The file name is chosen automatically based on the attachment id. * @param sender for mapping an exception which might occur to the sender * @param attachment the attachment to download * @param progressListener an optional download progress listener * @return the file descriptor for the saved attachment * @throws IOException */ public File saveAttachment(SignalServiceAddress sender, SignalServiceAttachment attachment, ProgressListener progressListener) throws IOException { return saveAttachment(toUser(sender), attachment, progressListener); } /** * Save an attachment to the attachments folder specified by {@code ATTACHMENTS_PATH}. * The file name is chosen automatically based on the attachment id. * @param sender for mapping an exception which might occur to the sender * @param attachment the attachment to download * @param progressListener an optional download progress listener * @return the file descriptor for the saved attachment * @throws IOException */ public File saveAttachment(User sender, SignalServiceAttachment attachment, ProgressListener progressListener) throws IOException { String attachmentId = String.valueOf(attachment.asPointer().getId()); return saveAttachment(sender, attachment, progressListener, attachmentId); } private File saveAttachment(User sender, SignalServiceAttachment attachment, ProgressListener progressListener, String attachmentId) throws IOException { File attachmentsDir = new File(ATTACHMENTS_PATH); if(!attachmentsDir.exists()) { boolean success = attachmentsDir.mkdirs(); if(!success) { throw new IOException("Could not create attachments directory!"); } } File file = Paths.get(attachmentsDir.getAbsolutePath(), attachmentId).toFile(); if(file.exists()) { return file; } File buffer = Paths.get(attachmentsDir.getAbsolutePath(), attachmentId + ".part").toFile(); InputStream in = null; if(attachment.isPointer()) { try { in = messageReceiver.retrieveAttachment(attachment.asPointer(), buffer, progressListener); } catch (InvalidMessageException e) { fireSecurityException(sender, e); } } else { in = attachment.asStream().getInputStream(); } Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING); buffer.delete(); return file; } /** * Convenience method to delete attachments * @param id */ public void deleteAttachment(String id) { File attachment = Paths.get(ATTACHMENTS_PATH, id).toFile(); if(attachment.exists()) { attachment.delete(); } } /** * Convenience method to get a file handle for an already saved attachment. * @param id * @return the file or null if no corresponding file is cached */ public File getAttachment(String id) { File attachment = Paths.get(ATTACHMENTS_PATH, id).toFile(); if(attachment.exists()) { return attachment; } else { return null; } } /** * Returns the data store where the contacts and groups are stored.
* There are two stores (key store (private) and data store), both saved in {@code STORE_PATH}. Both stores are managed by the library, * so you should only use this store for reading it. If you have to change something manually, call {@code save()} * afterwards. * @return the data store */ public DataStore getDataStore() { return store.getDataStore(); } /** * Leaves the group.

* Hack: You can use a custom crafted group. This can be useful to leave a group where you lost the store for. * @param group * @throws IOException */ public void leaveGroup(Group group) throws IOException { SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .withTimestamp(System.currentTimeMillis()) .asGroupMessage(SignalServiceGroup.newBuilder(Type.QUIT) .withId(group.getId().getId()) .build()) .build(); sendMessage(group.getMembers(), message); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy