package me.brandonc.security.totp.web;
import static me.brandonc.security.totp.util.PasswordUtils.encrypt;
import static me.brandonc.security.totp.util.TOTPUtils.checkCode;
import static me.brandonc.security.totp.util.TOTPUtils.generateSecret;
import java.beans.PropertyEditorSupport;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Hashtable;
import javax.servlet.http.HttpServletResponse;
import me.brandonc.security.totp.domain.Account;
import me.brandonc.security.totp.repository.AccountRepository;
import me.brandonc.security.totp.repository.QRCodeTicketRepository;
import me.brandonc.security.totp.web.response.ApplicationStatus;
import me.brandonc.security.totp.web.response.ResponseMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
@Controller
@RequestMapping(value = "/account")
public class AccountController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
@Autowired
private AccountRepository acccountRepository;
@Autowired
private QRCodeTicketRepository qrCodeTicketRepository;
@Value("${qrcode.width}")
private int qrcodeWidth = 400;
@Value("${qrcode.height}")
private int qrcodeHeight = 400;
@Value("${totp.hostLabel}")
private String hostLabel = "brandonc.me";
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.registerCustomEditor(String.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
if (text == null) {
return;
}
setValue(text.replaceAll("<.[^<>]*?>", ""));
}
@Override
public String getAsText() {
Object value = getValue();
return (value != null ? value.toString() : "");
}
});
}
@RequestMapping(value = "", method = RequestMethod.POST)
@ResponseBody
public ResponseMessage create(String name, String password) {
if (acccountRepository.exist(name)) {
return new ResponseMessage(ApplicationStatus.EXISTED, "The account already exist");
}
Account account = new Account();
account.setName(name);
account.setPassword(encrypt(password, name));
account.setSecret(generateSecret());
account.setCreated(new Date());
acccountRepository.addAccount(account);
qrCodeTicketRepository.createTicket(name);
return ResponseMessage.SUCCESSED;
}
@RequestMapping(value = "{name}/verify", method = RequestMethod.POST)
@ResponseBody
public ResponseMessage verifyCode(@PathVariable String name, @RequestParam String password, @RequestParam long code) throws InvalidKeyException, NoSuchAlgorithmException {
Account account = acccountRepository.findAccountByName(name);
if (account != null && checkCode(account.getSecret(), code) && checkPassword(account, password)) {
return ResponseMessage.SUCCESSED;
}
return new ResponseMessage(ApplicationStatus.FAILED, "Verification failed");
}
@RequestMapping(value = "{name}/qrcode", method = RequestMethod.GET)
public void showQrcode(@PathVariable String name, HttpServletResponse response) throws IllegalAccessException {
if (!qrCodeTicketRepository.useTicket(name)) {
throw new IllegalAccessException("no permission");
}
Account account = acccountRepository.findAccountByName(name);
response.setHeader("Cache-Control", "no-store");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/png");
String data = getQRBarcodeURL(account.getName(), hostLabel, account.getSecret());
BitMatrix matrix = null;
com.google.zxing.Writer writer = new MultiFormatWriter();
try {
Hashtable<EncodeHintType, String> hints = new Hashtable<EncodeHintType, String>(2);
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
matrix = writer.encode(data, com.google.zxing.BarcodeFormat.QR_CODE, qrcodeWidth, qrcodeHeight, hints);
} catch (com.google.zxing.WriterException e) {
logger.error(e.getMessage());
}
try {
MatrixToImageWriter.writeToStream(matrix, "PNG", response.getOutputStream());
} catch (IOException e) {
logger.error(e.getMessage());
}
}
private boolean checkPassword(Account account, String password) throws NoSuchAlgorithmException, InvalidKeyException {
return account.getPassword().equals(encrypt(password, account.getName()));
}
private String getQRBarcodeURL(String user, String host, String secret) {
String format = "otpauth://totp/%s@%s?secret=%s";
return String.format(format, user, host, secret);
}
}