Squashed History

This commit is contained in:
Moxie Marlinspike
2013-12-08 23:11:09 -08:00
commit 4ad0dad3d9
103 changed files with 9737 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import java.io.Serializable;
public class Account implements Serializable {
public static final int MEMCACHE_VERION = 1;
private long id;
private String number;
private String hashedAuthenticationToken;
private String salt;
private String signalingKey;
private String gcmRegistrationId;
private String apnRegistrationId;
private boolean supportsSms;
public Account() {}
public Account(long id, String number, String hashedAuthenticationToken, String salt,
String signalingKey, String gcmRegistrationId, String apnRegistrationId,
boolean supportsSms)
{
this.id = id;
this.number = number;
this.hashedAuthenticationToken = hashedAuthenticationToken;
this.salt = salt;
this.signalingKey = signalingKey;
this.gcmRegistrationId = gcmRegistrationId;
this.apnRegistrationId = apnRegistrationId;
this.supportsSms = supportsSms;
}
public String getApnRegistrationId() {
return apnRegistrationId;
}
public void setApnRegistrationId(String apnRegistrationId) {
this.apnRegistrationId = apnRegistrationId;
}
public String getGcmRegistrationId() {
return gcmRegistrationId;
}
public void setGcmRegistrationId(String gcmRegistrationId) {
this.gcmRegistrationId = gcmRegistrationId;
}
public void setNumber(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.hashedAuthenticationToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();
}
public AuthenticationCredentials getAuthenticationCredentials() {
return new AuthenticationCredentials(hashedAuthenticationToken, salt);
}
public String getSignalingKey() {
return signalingKey;
}
public void setSignalingKey(String signalingKey) {
this.signalingKey = signalingKey;
}
public boolean getSupportsSms() {
return supportsSms;
}
public void setSupportsSms(boolean supportsSms) {
this.supportsSms = supportsSms;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.TransactionIsolationLevel;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.Binder;
import org.skife.jdbi.v2.sqlobject.BinderFactory;
import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
import org.skife.jdbi.v2.sqlobject.GetGeneratedKeys;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
public abstract class Accounts {
public static final String ID = "id";
public static final String NUMBER = "number";
public static final String AUTH_TOKEN = "auth_token";
public static final String SALT = "salt";
public static final String SIGNALING_KEY = "signaling_key";
public static final String GCM_ID = "gcm_id";
public static final String APN_ID = "apn_id";
public static final String SUPPORTS_SMS = "supports_sms";
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + AUTH_TOKEN + ", " +
SALT + ", " + SIGNALING_KEY + ", " + GCM_ID + ", " +
APN_ID + ", " + SUPPORTS_SMS + ") " +
"VALUES (:number, :auth_token, :salt, :signaling_key, :gcm_id, :apn_id, :supports_sms)")
@GetGeneratedKeys
abstract long createStep(@AccountBinder Account account);
@SqlUpdate("DELETE FROM accounts WHERE number = :number")
abstract void removeStep(@Bind("number") String number);
@SqlUpdate("UPDATE accounts SET " + AUTH_TOKEN + " = :auth_token, " + SALT + " = :salt, " +
SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " +
APN_ID + " = :apn_id, " + SUPPORTS_SMS + " = :supports_sms " +
"WHERE " + NUMBER + " = :number")
abstract void update(@AccountBinder Account account);
@Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number")
abstract Account get(@Bind("number") String number);
@SqlQuery("SELECT COUNT(*) from accounts")
abstract long getCount();
@Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts OFFSET :offset LIMIT :limit")
abstract List<Account> getAll(@Bind("offset") int offset, @Bind("limit") int length);
@Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts")
abstract Iterator<Account> getAll();
@Transaction(TransactionIsolationLevel.REPEATABLE_READ)
public long create(Account account) {
removeStep(account.getNumber());
return createStep(account);
}
public static class AccountMapper implements ResultSetMapper<Account> {
@Override
public Account map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException
{
return new Account(resultSet.getLong(ID), resultSet.getString(NUMBER),
resultSet.getString(AUTH_TOKEN), resultSet.getString(SALT),
resultSet.getString(SIGNALING_KEY), resultSet.getString(GCM_ID),
resultSet.getString(APN_ID),
resultSet.getInt(SUPPORTS_SMS) == 1);
}
}
@BindingAnnotation(AccountBinder.AccountBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface AccountBinder {
public static class AccountBinderFactory implements BinderFactory {
@Override
public Binder build(Annotation annotation) {
return new Binder<AccountBinder, Account>() {
@Override
public void bind(SQLStatement<?> sql,
AccountBinder accountBinder,
Account account)
{
sql.bind(ID, account.getId());
sql.bind(NUMBER, account.getNumber());
sql.bind(AUTH_TOKEN, account.getAuthenticationCredentials()
.getHashedAuthenticationToken());
sql.bind(SALT, account.getAuthenticationCredentials().getSalt());
sql.bind(SIGNALING_KEY, account.getSignalingKey());
sql.bind(GCM_ID, account.getGcmRegistrationId());
sql.bind(APN_ID, account.getApnRegistrationId());
sql.bind(SUPPORTS_SMS, account.getSupportsSms() ? 1 : 0);
}
};
}
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Iterator;
import java.util.List;
public class AccountsManager {
private final Accounts accounts;
private final MemcachedClient memcachedClient;
private final DirectoryManager directory;
public AccountsManager(Accounts accounts,
DirectoryManager directory,
MemcachedClient memcachedClient)
{
this.accounts = accounts;
this.directory = directory;
this.memcachedClient = memcachedClient;
}
public long getCount() {
return accounts.getCount();
}
public List<Account> getAll(int offset, int length) {
return accounts.getAll(offset, length);
}
public Iterator<Account> getAll() {
return accounts.getAll();
}
public void create(Account account) {
long id = accounts.create(account);
account.setId(id);
if (memcachedClient != null) {
memcachedClient.set(getKey(account.getNumber()), 0, account);
}
updateDirectory(account);
}
public void update(Account account) {
if (memcachedClient != null) {
memcachedClient.set(getKey(account.getNumber()), 0, account);
}
accounts.update(account);
updateDirectory(account);
}
public Optional<Account> get(String number) {
Account account = null;
if (memcachedClient != null) {
account = (Account)memcachedClient.get(getKey(number));
}
if (account == null) {
account = accounts.get(number);
if (account != null && memcachedClient != null) {
memcachedClient.set(getKey(number), 0, account);
}
}
if (account != null) return Optional.of(account);
else return Optional.absent();
}
private void updateDirectory(Account account) {
if (account.getGcmRegistrationId() != null || account.getApnRegistrationId() != null) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
directory.add(clientContact);
} else {
directory.remove(account.getNumber());
}
}
private String getKey(String number) {
return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number;
}
}

View File

@@ -0,0 +1,164 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.LinkedList;
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
public class DirectoryManager {
private static final byte[] DIRECTORY_KEY = {'d', 'i', 'r', 'e', 'c', 't', 'o', 'r', 'y'};
private final JedisPool redisPool;
public DirectoryManager(JedisPool redisPool) {
this.redisPool = redisPool;
}
public void remove(String number) {
remove(Util.getContactToken(number));
}
public void remove(BatchOperationHandle handle, String number) {
remove(handle, Util.getContactToken(number));
}
public void remove(byte[] token) {
Jedis jedis = redisPool.getResource();
jedis.hdel(DIRECTORY_KEY, token);
redisPool.returnResource(jedis);
}
public void remove(BatchOperationHandle handle, byte[] token) {
Pipeline pipeline = handle.pipeline;
pipeline.hdel(DIRECTORY_KEY, token);
}
public void add(ClientContact contact) {
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
Jedis jedis = redisPool.getResource();
jedis.hset(DIRECTORY_KEY, contact.getToken(), new Gson().toJson(tokenValue).getBytes());
redisPool.returnResource(jedis);
}
public void add(BatchOperationHandle handle, ClientContact contact) {
Pipeline pipeline = handle.pipeline;
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
pipeline.hset(DIRECTORY_KEY, contact.getToken(), new Gson().toJson(tokenValue).getBytes());
}
public Optional<ClientContact> get(byte[] token) {
Jedis jedis = redisPool.getResource();
try {
byte[] result = jedis.hget(DIRECTORY_KEY, token);
if (result == null) {
return Optional.absent();
}
TokenValue tokenValue = new Gson().fromJson(new String(result), TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
} finally {
redisPool.returnResource(jedis);
}
}
public List<ClientContact> get(List<byte[]> tokens) {
Jedis jedis = redisPool.getResource();
try {
Pipeline pipeline = jedis.pipelined();
List<Response<byte[]>> futures = new LinkedList<>();
List<ClientContact> results = new LinkedList<>();
try {
for (byte[] token : tokens) {
futures.add(pipeline.hget(DIRECTORY_KEY, token));
}
} finally {
pipeline.sync();
}
IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures);
for (IterablePair.Pair<byte[], Response<byte[]>> pair : lists) {
if (pair.second().get() != null) {
TokenValue tokenValue = new Gson().fromJson(new String(pair.second().get()), TokenValue.class);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms);
results.add(clientContact);
}
}
return results;
} finally {
redisPool.returnResource(jedis);
}
}
public BatchOperationHandle startBatchOperation() {
Jedis jedis = redisPool.getResource();
return new BatchOperationHandle(jedis, jedis.pipelined());
}
public void stopBatchOperation(BatchOperationHandle handle) {
Pipeline pipeline = handle.pipeline;
Jedis jedis = handle.jedis;
pipeline.sync();
redisPool.returnResource(jedis);
}
public static class BatchOperationHandle {
public final Pipeline pipeline;
public final Jedis jedis;
public BatchOperationHandle(Jedis jedis, Pipeline pipeline) {
this.pipeline = pipeline;
this.jedis = jedis;
}
}
private static class TokenValue {
@SerializedName("r")
private String relay;
@SerializedName("s")
private boolean supportsSms;
public TokenValue(String relay, boolean supportsSms) {
this.relay = relay;
this.supportsSms = supportsSms;
}
}
}

View File

@@ -0,0 +1,121 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.TransactionIsolationLevel;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.Binder;
import org.skife.jdbi.v2.sqlobject.BinderFactory;
import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
import org.skife.jdbi.v2.sqlobject.SqlBatch;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.whispersystems.textsecuregcm.entities.PreKey;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
public abstract class Keys {
@SqlUpdate("DELETE FROM keys WHERE number = :number")
abstract void removeKeys(@Bind("number") String number);
@SqlUpdate("DELETE FROM keys WHERE id = :id")
abstract void removeKey(@Bind("id") long id);
@SqlBatch("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)")
abstract void append(@PreKeyBinder List<PreKey> preKeys);
@SqlUpdate("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)")
abstract void append(@PreKeyBinder PreKey preKey);
@SqlQuery("SELECT * FROM keys WHERE number = :number ORDER BY id LIMIT 1 FOR UPDATE")
@Mapper(PreKeyMapper.class)
abstract PreKey retrieveFirst(@Bind("number") String number);
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
public void store(String number, PreKey lastResortKey, List<PreKey> keys) {
for (PreKey key : keys) {
key.setNumber(number);
}
lastResortKey.setNumber(number);
removeKeys(number);
append(keys);
append(lastResortKey);
}
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
public PreKey get(String number) {
PreKey preKey = retrieveFirst(number);
if (preKey != null && !preKey.isLastResort()) {
removeKey(preKey.getId());
}
return preKey;
}
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface PreKeyBinder {
public static class PreKeyBinderFactory implements BinderFactory {
@Override
public Binder build(Annotation annotation) {
return new Binder<PreKeyBinder, PreKey>() {
@Override
public void bind(SQLStatement<?> sql, PreKeyBinder accountBinder, PreKey preKey)
{
sql.bind("id", preKey.getId());
sql.bind("number", preKey.getNumber());
sql.bind("key_id", preKey.getKeyId());
sql.bind("public_key", preKey.getPublicKey());
sql.bind("identity_key", preKey.getIdentityKey());
sql.bind("last_resort", preKey.isLastResort() ? 1 : 0);
}
};
}
}
}
public static class PreKeyMapper implements ResultSetMapper<PreKey> {
@Override
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException
{
return new PreKey(resultSet.getLong("id"), resultSet.getString("number"),
resultSet.getLong("key_id"), resultSet.getString("public_key"),
resultSet.getString("identity_key"),
resultSet.getInt("last_resort") == 1);
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
public interface PendingAccounts {
@SqlUpdate("WITH upsert AS (UPDATE pending_accounts SET verification_code = :verification_code WHERE number = :number RETURNING *) " +
"INSERT INTO pending_accounts (number, verification_code) SELECT :number, :verification_code WHERE NOT EXISTS (SELECT * FROM upsert)")
void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode);
@SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number")
String getCodeForNumber(@Bind("number") String number);
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
public class PendingAccountsManager {
private static final String MEMCACHE_PREFIX = "pending_account";
private final PendingAccounts pendingAccounts;
private final MemcachedClient memcachedClient;
public PendingAccountsManager(PendingAccounts pendingAccounts,
MemcachedClient memcachedClient)
{
this.pendingAccounts = pendingAccounts;
this.memcachedClient = memcachedClient;
}
public void store(String number, String code) {
if (memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
pendingAccounts.insert(number, code);
}
public Optional<String> getCodeForNumber(String number) {
String code = null;
if (memcachedClient != null) {
code = (String)memcachedClient.get(MEMCACHE_PREFIX + number);
}
if (code == null) {
code = pendingAccounts.getCodeForNumber(number);
if (code != null && memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
}
if (code != null) return Optional.of(code);
else return Optional.absent();
}
}