/**
* Copyright 2013 Tommi S.E. Laukkanen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bubblecloud.ilves.security;
import com.vaadin.server.ExternalResource;
import com.vaadin.server.Sizeable;
import com.vaadin.ui.*;
import com.yubico.u2f.data.DeviceRegistration;
import org.apache.commons.codec.binary.Base32;
import org.bubblecloud.ilves.model.AuthenticationDevice;
import org.bubblecloud.ilves.model.AuthenticationDeviceType;
import org.bubblecloud.ilves.model.Company;
import org.bubblecloud.ilves.model.User;
import org.bubblecloud.ilves.site.SecurityProviderSessionImpl;
import org.bubblecloud.ilves.site.Site;
import org.bubblecloud.ilves.site.SiteContext;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.persistence.EntityManager;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
/**
* Google authenticator service for two factor authentication with Google Authenticator app.
*
* @author Tommi S.E. Laukkanen
*/
public class GoogleAuthenticatorService {
/**
* Cryptographic hash function used to calculate the HMAC (Hash-based
* Message Authentication Code). This implementation uses the SHA1 hash
* function.
*/
private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
/**
* Generates secret key and MIME encodes it.
*
* @return the MIME encoded secret key
*/
public static String generateSecretKey() {
final int secretSize = 10;
final byte[] buffer = new byte[secretSize];
new Random().nextBytes(buffer);
// Getting the key and converting it to Base32
final Base32 codec = new Base32();
final byte[] secretKey = Arrays.copyOf(buffer, secretSize);
final byte[] bEncodedKey = codec.encode(secretKey);
final String encodedKey = new String(bEncodedKey);
return encodedKey;
}
/**
* Get QR Barcode URL
* @param user the user
* @param host the host
* @param secretKey the secret key
* @return the QR Barcode URL
*/
public static String getQRBarcodeURL(
final String user,
final String host,
final String secretKey) {
String format = "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s/%s%%3Fsecret%%3D%s";
return String.format(format, host, user, secretKey);
}
/**
* This method implements the algorithm specified in RFC 6238 to check if a
* validation code is valid in a given instant of time for the given secret
* key.
*
* @param secret the Base32 encoded secret key.
* @param codeString the code to validate.
* @return <code>true</code> if the validation code is valid,
* <code>false</code> otherwise.
*/
public static boolean checkCode(final String secret, final String codeString) {
final long code;
try {
code = Long.parseLong(codeString);
} catch(final NumberFormatException e) {
return false;
}
final Base32 codec32 = new Base32();
final byte[] decodedKey = codec32.decode(secret);
final long timeWindow = System.currentTimeMillis() / 30000;
final int window = 0;
for (int i = -((window - 1) / 2); i <= window / 2; ++i) {
final long hash = calculateCode(decodedKey, timeWindow + i);
if (hash == code) {
return true;
}
}
return false;
}
/**
* Calculates the verification code of the provided key at the specified
* instant of time using the algorithm specified in RFC 6238.
*
* @param key the secret key in binary format.
* @param tm the instant of time.
* @return the validation code for the provided key at the specified instant
* of time.
*/
private static int calculateCode(final byte[] key, final long tm) {
final byte[] data = new byte[8];
long value = tm;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
final SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);
try {
final Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);
mac.init(signKey);
final byte[] hash = mac.doFinal(data);
final int offset = hash[hash.length - 1] & 0xF;
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
truncatedHash |= (hash[offset + i] & 0xFF);
}
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= 1000000;
return (int) truncatedHash;
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new RuntimeException(ex);
}
}
/**
* Shows QR Code dialog.
* @param qrCodeUrl the QR code URL
*/
public static void showGrCodeDialog(final String qrCodeUrl) {
final Window subWindow = new Window(Site.getCurrent().localize("header-scan-qr-code-with-google-authenticator"));
subWindow.setModal(true);
final VerticalLayout verticalLayout = new VerticalLayout();
verticalLayout.setMargin(true);
final Image qrCodeImage = new Image(null, new ExternalResource(qrCodeUrl));
verticalLayout.addComponent(qrCodeImage);
verticalLayout.setComponentAlignment(qrCodeImage, Alignment.MIDDLE_CENTER);
subWindow.setContent(verticalLayout);
subWindow.setResizable(false);
subWindow.setWidth(230, Sizeable.Unit.PIXELS);
subWindow.setHeight(260, Sizeable.Unit.PIXELS);
subWindow.center();
UI.getCurrent().addWindow(subWindow);
}
/**
* Starts GoogleAuthenticator device registration.
*
* @param googleAuthenticatorRegistrationListener the listener
*/
public static void startRegistration(final GoogleAuthenticatorRegistrationListener googleAuthenticatorRegistrationListener) {
final Site site = Site.getCurrent();
final SiteContext securityContext = site.getSiteContext();
final Company company = securityContext.getObject(Company.class);
final User user = ((SecurityProviderSessionImpl)
site.getSecurityProvider()).getUserFromSession();
final String secretKey = generateSecretKey();
SecurityService.updateUser(securityContext, securityContext.getEntityManager().merge(user));
final String qrCodeUrl;
try {
qrCodeUrl = getQRBarcodeURL(user.getEmailAddress(), new URL(company.getUrl()).getHost(), secretKey);
} catch (MalformedURLException e) {
throw new RuntimeException("Invalid company URL format.", e);
}
showGrCodeDialog(qrCodeUrl);
addDeviceRegistration(securityContext, user.getEmailAddress(), secretKey);
googleAuthenticatorRegistrationListener.onDeviceRegistrationSuccess();
}
/**
* Adds device registration.
* @param context the context
* @param emailAddress the email address
* @param secret the device secret
*/
public static void addDeviceRegistration(final SiteContext context, final String emailAddress, final String secret) {
final Company company = context.getObject(Company.class);
final EntityManager entityManager = context.getEntityManager();
final User user = UserDao.getUser(entityManager, company, emailAddress);
final String encryptedSecret = SecurityUtil.encryptSecretKey(secret);
final AuthenticationDevice authenticationDevice = new AuthenticationDevice();
final Date now = new Date();
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss,SSS");
final String deviceKey = emailAddress + "-ga-" + now.getTime();
final String deviceName = "Google Authenticator " + simpleDateFormat.format(now);
authenticationDevice.setKey(deviceKey);
authenticationDevice.setName(deviceName);
authenticationDevice.setType(AuthenticationDeviceType.GOOGLE_AUTHENTICATOR);
authenticationDevice.setUser(user);
authenticationDevice.setEncryptedSecret(encryptedSecret);
AuthenticationDeviceDao.addAuthenticationDevice(entityManager, authenticationDevice);
}
}