/** * 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 . */ package org.whispersystems.textsecuregcm.controllers; import com.google.common.base.Optional; import com.yammer.dropwizard.auth.Auth; import com.yammer.metrics.annotation.Timed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthorizationHeader; import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.sms.SenderFactory; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.VerificationCode; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @Path("/v1/accounts") public class AccountController { private final Logger logger = LoggerFactory.getLogger(AccountController.class); private final PendingAccountsManager pendingAccounts; private final AccountsManager accounts; private final RateLimiters rateLimiters; private final SenderFactory senderFactory; public AccountController(PendingAccountsManager pendingAccounts, AccountsManager accounts, RateLimiters rateLimiters, SenderFactory smsSenderFactory) { this.pendingAccounts = pendingAccounts; this.accounts = accounts; this.rateLimiters = rateLimiters; this.senderFactory = smsSenderFactory; } @Timed @GET @Path("/{transport}/code/{number}") public Response createAccount(@PathParam("transport") String transport, @PathParam("number") String number) throws IOException, RateLimitExceededException { if (!Util.isValidNumber(number)) { logger.debug("Invalid number: " + number); throw new WebApplicationException(Response.status(400).build()); } switch (transport) { case "sms": rateLimiters.getSmsDestinationLimiter().validate(number); break; case "voice": rateLimiters.getVoiceDestinationLimiter().validate(number); break; default: throw new WebApplicationException(Response.status(415).build()); } VerificationCode verificationCode = generateVerificationCode(); pendingAccounts.store(number, verificationCode.getVerificationCode()); if (transport.equals("sms")) { senderFactory.getSmsSender(number).deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay()); } else if (transport.equals("voice")) { senderFactory.getVoxSender(number).deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech()); } return Response.ok().build(); } @Timed @PUT @Consumes(MediaType.APPLICATION_JSON) @Path("/code/{verification_code}") public void verifyAccount(@PathParam("verification_code") String verificationCode, @HeaderParam("Authorization") String authorizationHeader, @Valid AccountAttributes accountAttributes) throws RateLimitExceededException { try { AuthorizationHeader header = new AuthorizationHeader(authorizationHeader); String number = header.getUserName(); String password = header.getPassword(); rateLimiters.getVerifyLimiter().validate(number); Optional storedVerificationCode = pendingAccounts.getCodeForNumber(number); if (!storedVerificationCode.isPresent() || !verificationCode.equals(storedVerificationCode.get())) { throw new WebApplicationException(Response.status(403).build()); } Account account = new Account(); account.setNumber(number); account.setAuthenticationCredentials(new AuthenticationCredentials(password)); account.setSignalingKey(accountAttributes.getSignalingKey()); account.setSupportsSms(accountAttributes.getSupportsSms()); accounts.create(account); logger.debug("Stored account..."); } catch (InvalidAuthorizationHeaderException e) { logger.info("Bad Authorization Header", e); throw new WebApplicationException(Response.status(401).build()); } } @Timed @PUT @Path("/gcm/") @Consumes(MediaType.APPLICATION_JSON) public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) { account.setApnRegistrationId(null); account.setGcmRegistrationId(registrationId.getGcmRegistrationId()); accounts.update(account); } @Timed @DELETE @Path("/gcm/") public void deleteGcmRegistrationId(@Auth Account account) { account.setGcmRegistrationId(null); accounts.update(account); } @Timed @PUT @Path("/apn/") @Consumes(MediaType.APPLICATION_JSON) public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) { account.setApnRegistrationId(registrationId.getApnRegistrationId()); account.setGcmRegistrationId(null); accounts.update(account); } @Timed @DELETE @Path("/apn/") public void deleteApnRegistrationId(@Auth Account account) { account.setApnRegistrationId(null); accounts.update(account); } private VerificationCode generateVerificationCode() { try { SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); int randomInt = 100000 + random.nextInt(900000); return new VerificationCode(randomInt); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } } }