Browse Source

Merge pull request #6 from englishtown/feature/encrypt-password

Added password encryption (fixes #1)
pull/7/merge
Adrian 12 years ago
parent
commit
c1a8e69501
  1. 115
      src/main/java/com/englishtown/stash/hook/DefaultPasswordEncryptor.java
  2. 57
      src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java
  3. 18
      src/main/java/com/englishtown/stash/hook/PasswordEncryptor.java
  4. 4
      src/main/resources/atlassian-plugin.xml
  5. 129
      src/test/java/com/englishtown/stash/hook/DefaultPasswordEncryptorTest.java
  6. 17
      src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java

115
src/main/java/com/englishtown/stash/hook/DefaultPasswordEncryptor.java

@ -0,0 +1,115 @@
package com.englishtown.stash.hook;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* Service to encrypt/decrypt git user passwords
*/
public class DefaultPasswordEncryptor implements PasswordEncryptor {
private SecretKey secretKey;
public static final String ENCRYPTED_PREFIX = "encrypted:";
public static final String SETTINGS_CRYPTO_KEY = "crypto.key";
@Override
public void init(PluginSettings pluginSettings) {
try {
String keyBase64;
Object value = pluginSettings.get(SETTINGS_CRYPTO_KEY);
if (value == null || value.toString().isEmpty()) {
KeyGenerator gen = KeyGenerator.getInstance("AES");
secretKey = gen.generateKey();
keyBase64 = Base64.encodeBase64String(secretKey.getEncoded());
pluginSettings.put(SETTINGS_CRYPTO_KEY, keyBase64);
} else {
keyBase64 = value.toString();
byte[] data = Base64.decodeBase64(keyBase64);
secretKey = new SecretKeySpec(data, 0, data.length, "AES");
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
protected byte[] runCipher(byte[] data, boolean encrypt) {
try {
int mode = encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE;
Cipher cipher = getCipher(mode);
return cipher.doFinal(data);
} catch (IllegalBlockSizeException e) {
throw new RuntimeException(e);
} catch (BadPaddingException e) {
throw new RuntimeException(e);
}
}
private Cipher getCipher(int mode) {
try {
// Create Cipher
Cipher cipher = Cipher.getInstance("AES");
// Initialize Cipher with key
cipher.init(mode, secretKey);
return cipher;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (NoSuchPaddingException e) {
throw new RuntimeException(e);
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isEncrypted(String password) {
if (password == null || password.isEmpty()) {
return false;
}
return password.startsWith(ENCRYPTED_PREFIX);
}
@Override
public String encrypt(String password) {
if (isEncrypted(password)) {
return password;
}
try {
byte[] encryptedData = runCipher(password.getBytes("UTF-8"), true);
return ENCRYPTED_PREFIX + Base64.encodeBase64String(encryptedData);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public String decrypt(String password) {
if (!isEncrypted(password)) {
return password;
}
try {
byte[] encryptedData = Base64.decodeBase64(password.substring(ENCRYPTED_PREFIX.length()));
byte[] clearData = runCipher(encryptedData, false);
return new String(clearData, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}

57
src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java

@ -1,5 +1,7 @@
package com.englishtown.stash.hook;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
import com.atlassian.stash.hook.repository.AsyncPostReceiveRepositoryHook;
import com.atlassian.stash.hook.repository.RepositoryHookContext;
import com.atlassian.stash.i18n.I18nService;
@ -16,15 +18,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator {
public static final String PLUGIN_SETTINGS_KEY = "com.englishtown.stash.hook.mirror";
static final String SETTING_MIRROR_REPO_URL = "mirrorRepoUrl";
static final String SETTING_USERNAME = "username";
static final String SETTING_PASSWORD = "password";
@ -33,15 +39,29 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
private final GitScm gitScm;
private final I18nService i18nService;
private final ScheduledExecutorService executor;
private final PasswordEncryptor passwordEncryptor;
private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class);
public MirrorRepositoryHook(
GitScm gitScm,
I18nService i18nService,
ScheduledExecutorService executor) {
ScheduledExecutorService executor,
PasswordEncryptor passwordEncryptor,
PluginSettingsFactory pluginSettingsFactory
) {
logger.debug("MirrorRepositoryHook: init started");
// Set fields
this.gitScm = gitScm;
this.i18nService = i18nService;
this.executor = executor;
this.passwordEncryptor = passwordEncryptor;
// Init password encryptor
PluginSettings pluginSettings = pluginSettingsFactory.createSettingsForKey(PLUGIN_SETTINGS_KEY);
passwordEncryptor.init(pluginSettings);
logger.debug("MirrorRepositoryHook: init completed");
}
/**
@ -71,9 +91,10 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
}
void runMirrorCommand(String mirrorRepoUrl, String username, final String password, final Repository repository) {
void runMirrorCommand(String mirrorRepoUrl, String username, final String encryptedPassword, final Repository repository) {
try {
final String password = passwordEncryptor.decrypt(encryptedPassword);
final URI authenticatedUrl = getAuthenticatedUrl(mirrorRepoUrl, username, password);
executor.submit(new Callable<Void>() {
@ -179,6 +200,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
// If no errors, run the mirror command
if (count == 0) {
encryptPassword(settings, SETTING_PASSWORD, password);
runMirrorCommand(mirrorRepoUrl, username, password, repository);
}
logger.debug("MirrorRepositoryHook: validate completed with {} error(s).", count);
@ -190,4 +212,35 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
}
void encryptPassword(Settings settings, String settingName, String password) {
// Skip if already encrypted
if (passwordEncryptor.isEncrypted(password)) {
return;
}
try {
// Unfortunately the settings are stored in an immutable map, so need to cheat with reflection
logger.info("Encrypting password");
Field field = settings.getClass().getDeclaredField("values");
field.setAccessible(true);
// Create mutable copy of values, and encrypt the password
@SuppressWarnings("unchecked")
Map<String, Object> values = (Map<String, Object>) field.get(settings);
values = new HashMap<String, Object>(values);
values.put(settingName, passwordEncryptor.encrypt(password));
field.set(settings, values);
} catch (NoSuchFieldException e) {
throw new RuntimeException("Unable to encrypt the password. Check for an updated version of the mirror " +
"hook.", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to encrypt the password. Check for an updated version of the mirror " +
"hook.", e);
}
}
}

18
src/main/java/com/englishtown/stash/hook/PasswordEncryptor.java

@ -0,0 +1,18 @@
package com.englishtown.stash.hook;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
/**
* Service to encrypt/decrypt git user passwords
*/
public interface PasswordEncryptor {
void init(PluginSettings pluginSettings);
boolean isEncrypted(String password);
String encrypt(String password);
String decrypt(String password);
}

4
src/main/resources/atlassian-plugin.xml

@ -11,6 +11,10 @@
<component-import key="gitScm" interface="com.atlassian.stash.scm.git.GitScm"/>
<component-import key="i18nService" interface="com.atlassian.stash.i18n.I18nService"/>
<component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties"/>
<component-import key="pluginSettingsFactory" interface="com.atlassian.sal.api.pluginsettings.PluginSettingsFactory"/>
<component key="passwordEncryptor" class="com.englishtown.stash.hook.DefaultPasswordEncryptor">
<interface>com.englishtown.stash.hook.PasswordEncryptor</interface>
</component>
<!-- add our i18n resource -->
<resource type="i18n" name="i18n" location="stash-hook-mirror"/>

129
src/test/java/com/englishtown/stash/hook/DefaultPasswordEncryptorTest.java

@ -0,0 +1,129 @@
package com.englishtown.stash.hook;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* DefaultPasswordEncryptor unit tests
*/
@RunWith(MockitoJUnitRunner.class)
public class DefaultPasswordEncryptorTest {
private final static String CRYPTO_KEY = "m3ys5YexQc7irRlmJeCwAw==";
@Mock
private PluginSettings pluginSettings;
private DefaultPasswordEncryptor encryptor;
@Before
public void setUp() throws Exception {
when(pluginSettings.get(DefaultPasswordEncryptor.SETTINGS_CRYPTO_KEY)).thenReturn(CRYPTO_KEY);
encryptor = new DefaultPasswordEncryptor();
encryptor.init(pluginSettings);
}
@Test
public void testInit() throws Exception {
DefaultPasswordEncryptor encryptor = new DefaultPasswordEncryptor();
PluginSettings pluginSettings = mock(PluginSettings.class);
encryptor.init(pluginSettings);
verify(pluginSettings).put(eq(DefaultPasswordEncryptor.SETTINGS_CRYPTO_KEY), anyString());
when(pluginSettings.get(DefaultPasswordEncryptor.SETTINGS_CRYPTO_KEY)).thenReturn(CRYPTO_KEY);
encryptor.init(pluginSettings);
// Verify put hasn't been called again
verify(pluginSettings).put(eq(DefaultPasswordEncryptor.SETTINGS_CRYPTO_KEY), anyString());
}
@Test
public void testRunCipher() throws Exception {
DefaultPasswordEncryptor encryptor = new DefaultPasswordEncryptor();
encryptor.init(pluginSettings);
String clearText = "clear text";
byte[] clearData = clearText.getBytes();
byte[] encryptedData;
byte[] resultData;
String resultText;
encryptedData = encryptor.runCipher(clearData, true);
resultData = encryptor.runCipher(encryptedData, false);
resultText = new String(resultData);
assertArrayEquals(clearData, resultData);
assertEquals(clearText, resultText);
}
@Test
public void testIsEncrypted() throws Exception {
DefaultPasswordEncryptor encryptor = new DefaultPasswordEncryptor();
String password = "clear-text-key";
boolean result;
result = encryptor.isEncrypted(password);
assertFalse(result);
password = null;
result = encryptor.isEncrypted(password);
assertFalse(result);
password = "";
result = encryptor.isEncrypted(password);
assertFalse(result);
password = DefaultPasswordEncryptor.ENCRYPTED_PREFIX + "encrypted-key";
result = encryptor.isEncrypted(password);
assertTrue(result);
}
@Test
public void testEncrypt() throws Exception {
String password = "test";
String encrypted;
String clear;
String result;
assertFalse(encryptor.isEncrypted(password));
encrypted = encryptor.encrypt(password);
assertTrue(encryptor.isEncrypted(encrypted));
result = encryptor.encrypt(encrypted);
assertEquals(encrypted, result);
clear = encryptor.decrypt(encrypted);
assertEquals(password, clear);
assertFalse(encryptor.isEncrypted(clear));
result = encryptor.decrypt(clear);
assertEquals(clear, result);
}
}

17
src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java

@ -1,5 +1,7 @@
package com.englishtown.stash.hook;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
import com.atlassian.stash.hook.repository.RepositoryHookContext;
import com.atlassian.stash.i18n.I18nService;
import com.atlassian.stash.repository.RefChange;
@ -43,6 +45,12 @@ public class MirrorRepositoryHookTest {
private GitCommand<String> cmd;
@Mock
private ScheduledExecutorService executor;
@Mock
private PasswordEncryptor passwordEncryptor;
@Mock
private PluginSettingsFactory pluginSettingsFactory;
@Mock
private PluginSettings pluginSettings;
private final String mirrorRepoUrl = "https://stash-mirror.englishtown.com/scm/test/test.git";
private final String username = "test-user";
@ -69,13 +77,17 @@ public class MirrorRepositoryHookTest {
GitScm gitScm = mock(GitScm.class);
when(gitScm.getCommandBuilderFactory()).thenReturn(builderFactory);
hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor);
when(pluginSettingsFactory.createSettingsForKey(anyString())).thenReturn(pluginSettings);
hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor, passwordEncryptor, pluginSettingsFactory);
}
@Test
public void testPostReceive() throws Exception {
when(passwordEncryptor.decrypt(anyString())).thenReturn(password);
Settings settings = mock(Settings.class);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL))).thenReturn(mirrorRepoUrl);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME))).thenReturn(username);
@ -99,7 +111,7 @@ public class MirrorRepositoryHookTest {
GitScm gitScm = mock(GitScm.class);
when(gitScm.getCommandBuilderFactory()).thenThrow(new RuntimeException("Intentional unit test exception"));
MirrorRepositoryHook hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor);
MirrorRepositoryHook hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor, passwordEncryptor, pluginSettingsFactory);
hook.runMirrorCommand(mirrorRepoUrl, username, password, mock(Repository.class));
verify(executor).submit(argumentCaptor.capture());
@ -200,6 +212,7 @@ public class MirrorRepositoryHookTest {
verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), anyString());
verify(errors).addFieldError(anyString(), anyString());
when(passwordEncryptor.isEncrypted(anyString())).thenReturn(true);
errors = mock(SettingsValidationErrors.class);
hook.validate(settings, errors, repo);
verify(errors, never()).addFormError(anyString());

Loading…
Cancel
Save