/**
* Copyright 2012, Board of Regents of the University of
* Wisconsin System. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Board of Regents of the University of Wisconsin
* System licenses this file to you 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 edu.wisc.doit.tcrypt.dao.impl;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.security.KeyPair;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bouncycastle.openssl.PEMWriter;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
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.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Repository;
import edu.wisc.doit.tcrypt.TokenKeyPairGenerator;
import edu.wisc.doit.tcrypt.dao.IKeysKeeper;
import edu.wisc.doit.tcrypt.vo.ServiceKey;
@Repository("keysKeeper")
public class KeysKeeper implements IKeysKeeper
{
private static final Pattern KEY_NAME_PATTERN = Pattern.compile("^([^_]+)_([^_]+)_(\\d{14})_(\\d+)_public\\.pem$");
private static final DateTimeFormatter KEY_CREATED_FORMATTER = DateTimeFormat.forPattern("yyyyMMddHHmmss");
protected final Logger logger = LoggerFactory.getLogger(KeysKeeper.class);
private final Map<String, ServiceKey> keysCache = new ConcurrentHashMap<String, ServiceKey>();
private final TokenKeyPairGenerator keyPairGenerator;
private final File directory;
private volatile DateTime lastScan = null;
/**
* Constructor
* @param directory Location of keys directory
* @param keyPairGenerator KeyPair Generator
*/
@Autowired
public KeysKeeper(@Value("${edu.wisc.doit.tcrypt.path.keydirectory:WEB-INF/keys}") String directoryname, TokenKeyPairGenerator keyPairGenerator)
{
this.directory = new File(directoryname);
if (!this.directory.exists()) {
if (!this.directory.mkdirs()) {
throw new IllegalArgumentException("Failed to create keys directory: '" + directoryname + "'");
}
}
if (!this.directory.isDirectory() || !this.directory.canRead() || !this.directory.canWrite()) {
throw new IllegalArgumentException("'" + directoryname + "' is not a directory that we have read/write access to");
}
this.keyPairGenerator = keyPairGenerator;
logger.info("key directory: {}", directoryname);
//Init keys cache
this.scanForKeys();
}
//Make sure this gets called at least once/hour
@Scheduled(fixedDelay=3600000)
public void scanForKeys() {
//Only re-scan every 1 minute at most
final DateTime now = DateTime.now();
if (this.lastScan != null && !this.lastScan.plusMinutes(1).isBefore(now)) {
return;
}
this.lastScan = now;
forcedScanForKeys();
}
private void forcedScanForKeys() {
try {
logger.debug("Scanning {} for updates to key files", directory);
final File[] keyfiles = directory.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (KEY_NAME_PATTERN.matcher(name).matches()) {
return true;
}
logger.warn("Ignoring {} in {}", name, dir);
return false;
}
});
final Set<String> oldKeys = new HashSet<String>(this.keysCache.keySet());
for (final File keyFile : keyfiles) {
final String keyName = keyFile.getName();
final Matcher keyNameMatcher = KEY_NAME_PATTERN.matcher(keyName);
if (!keyNameMatcher.matches()) {
throw new IllegalStateException("Some how " + keyFile + " matched the FilenameFilter but does not match the key name pattern");
}
logger.debug("Found key: {}", keyFile);
final String serviceName = keyNameMatcher.group(1);
//Record that we saw the service name and if it wasn't removed it must be a new key
if (!oldKeys.remove(serviceName)) {
final String createdByNetId = keyNameMatcher.group(2);
final String dayCreatedStr = keyNameMatcher.group(3);
final DateTime dayCreated = KEY_CREATED_FORMATTER.parseDateTime(dayCreatedStr);
final int keyLength = Integer.parseInt(keyNameMatcher.group(4));
final ServiceKey serviceKey = new ServiceKey(serviceName, keyLength, createdByNetId, dayCreated, keyFile);
this.keysCache.put(serviceName, serviceKey);
}
}
//Remove any keys that have been deleted from the file system
if (!oldKeys.isEmpty()) {
logger.info("Removed old service keys: {}", oldKeys);
this.keysCache.keySet().removeAll(oldKeys);
}
logger.info("Scanned {}, keysCache contains {} keys", directory, this.keysCache.size());
}
catch (Exception e) {
logger.error("Failed to scan {} for keys", this.directory, e);
}
}
@Override
public Set<String> getListOfServiceNames() {
return this.keysCache.keySet();
}
@Override
public KeyPair createServiceKey(String serviceName, int keyLength, String username) throws IOException {
if (this.keysCache.containsKey(serviceName)) {
throw new IllegalArgumentException("'" + serviceName + "' service key already exists.");
}
logger.debug("Generating {} bit KeyPair for service {} requested by {}", keyLength, serviceName, username);
final KeyPair keyPair = this.keyPairGenerator.generateKeyPair(keyLength);
// Build File Name
// Pattern: SERVICENAME_NETID_YYYYMMDDHHMMSS_KEYLENGTH_public.pem
final String fileName = serviceName + "_" +
username + "_" +
KEY_CREATED_FORMATTER.print(DateTime.now()) + "_" +
keyLength + "_public.pem";
final File publicKeyFile = new File(this.directory, fileName);
if (publicKeyFile.exists()) {
logger.warn("Key file already exists at {} it will be overwritten.", publicKeyFile);
publicKeyFile.delete();
}
try (final PEMWriter pemWriter = new PEMWriter(new BufferedWriter(new FileWriter(publicKeyFile)))) {
pemWriter.writeObject(keyPair.getPublic());
}
logger.info("Wrote new public key for {} to {}", serviceName, publicKeyFile);
this.forcedScanForKeys();
return keyPair;
}
@Override
public ServiceKey getServiceKey(String serviceName) {
return this.keysCache.get(serviceName);
}
}