Browse Source

SEC-1569: initial commit of spring-security-crypto module, consisting of encrypt, keygen, password, and util packages

Keith Donald 14 years ago
parent
commit
ffa7301e7f
29 changed files with 1254 additions and 1 deletions
  1. 5 0
      crypto/crypto.gradle
  2. 34 0
      crypto/src/main/java/org/springframework/security/crypto/encrypt/BytesEncryptor.java
  3. 86 0
      crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java
  4. 45 0
      crypto/src/main/java/org/springframework/security/crypto/encrypt/HexEncodingTextEncryptor.java
  5. 84 0
      crypto/src/main/java/org/springframework/security/crypto/encrypt/PasswordBasedBytesEncryptor.java
  6. 60 0
      crypto/src/main/java/org/springframework/security/crypto/encrypt/QueryableTextEncryptor.java
  7. 34 0
      crypto/src/main/java/org/springframework/security/crypto/encrypt/TextEncryptor.java
  8. 5 0
      crypto/src/main/java/org/springframework/security/crypto/encrypt/package-info.java
  9. 35 0
      crypto/src/main/java/org/springframework/security/crypto/keygen/BytesKeyGenerator.java
  10. 37 0
      crypto/src/main/java/org/springframework/security/crypto/keygen/HexEncodingStringKeyGenerator.java
  11. 54 0
      crypto/src/main/java/org/springframework/security/crypto/keygen/KeyGenerators.java
  12. 85 0
      crypto/src/main/java/org/springframework/security/crypto/keygen/SecureRandomBytesKeyGenerator.java
  13. 26 0
      crypto/src/main/java/org/springframework/security/crypto/keygen/StringKeyGenerator.java
  14. 6 0
      crypto/src/main/java/org/springframework/security/crypto/keygen/package-info.java
  15. 46 0
      crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java
  16. 40 0
      crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java
  17. 86 0
      crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java
  18. 5 0
      crypto/src/main/java/org/springframework/security/crypto/password/package-info.java
  19. 95 0
      crypto/src/main/java/org/springframework/security/crypto/util/CipherUtils.java
  20. 62 0
      crypto/src/main/java/org/springframework/security/crypto/util/Digester.java
  21. 122 0
      crypto/src/main/java/org/springframework/security/crypto/util/EncodingUtils.java
  22. 4 0
      crypto/src/main/java/org/springframework/security/crypto/util/package-info.java
  23. 49 0
      crypto/src/test/java/org/springframework/security/crypto/encrypt/EncryptorsTests.java
  24. 43 0
      crypto/src/test/java/org/springframework/security/crypto/keygen/KeyGeneratorsTests.java
  25. 25 0
      crypto/src/test/java/org/springframework/security/crypto/password/StandardPasswordEncoderTests.java
  26. 19 0
      crypto/src/test/java/org/springframework/security/crypto/util/DigesterTests.java
  27. 44 0
      crypto/src/test/java/org/springframework/security/crypto/util/EncodingUtilsTests.java
  28. 16 0
      crypto/template.mf
  29. 2 1
      settings.gradle

+ 5 - 0
crypto/crypto.gradle

@@ -0,0 +1,5 @@
+// crypto module build file
+
+dependencies {
+
+}

+ 34 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/BytesEncryptor.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.encrypt;
+
+/**
+ * Service interface for symmetric data encryption.
+ * @author Keith Donald
+ */
+public interface BytesEncryptor {
+
+    /**
+     * Encrypt the byte array.
+     */
+    byte[] encrypt(byte[] byteArray);
+
+    /**
+     * Decrypt the byte array.
+     */
+    byte[] decrypt(byte[] encryptedByteArray);
+
+}

+ 86 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java

@@ -0,0 +1,86 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.encrypt;
+
+/**
+ * Factory for commonly used encryptors.
+ * Defines the public API for constructing {@link BytesEncryptor} and {@link TextEncryptor} implementations.
+ * @author Keith Donald
+ */
+public class Encryptors {
+
+    /**
+     * Creates a standard password-based bytes encryptor.
+     * Uses MD5 PRF hashing with 1024 iterations and DES-based encryption.
+     * Salts each encrypted value to ensure it will be unique.
+     * TODO - switch standard algorithm from DES to AES.  Switch hashing to SHA-1 from MD5.
+     * @param password the password used to generate the encryptor's secret key; should not be shared
+     */
+    public static BytesEncryptor standard(String password) {
+        return new PasswordBasedBytesEncryptor(PBE_MD5_DES_ALGORITHM, password);
+    }
+
+    /**
+     * Creates a text encryptor that uses standard password-based encryption.
+     * Encrypted text is hex-encoded.
+     * @param password the password used to generate the encryptor's secret key; should not be shared
+     */
+    public static TextEncryptor text(String password) {
+        return new HexEncodingTextEncryptor(standard(password));
+    }
+
+    /**
+     * Creates an encryptor for queryable text strings that uses standard password-based encryption.
+     * The hex-encoded salt string provided should be random and is used to protect against password dictionary attacks.
+     * Does not salt each encrypted value so an encrypted value may be queried against.
+     * Encrypted text is hex-encoded.
+     * @param password the password used to generate the encryptor's secret key; should not be shared
+     * @param salt an hex-encoded, random, site-global salt value to use to initialize the cipher
+     */
+    public static TextEncryptor queryableText(String password, String salt) {
+        return new QueryableTextEncryptor(PBE_MD5_DES_ALGORITHM, password, salt);
+    }
+
+    /**
+     * Creates a text encrypter that performs no encryption.
+     * Useful for test environments where working with plain text strings is desired for simplicity.
+     */
+    public static TextEncryptor noOpText() {
+        return NO_OP_TEXT_INSTANCE;
+    }
+
+    // internal helpers
+
+    private Encryptors() {
+    }
+
+    private static final String PBE_MD5_DES_ALGORITHM = "PBEWithMD5AndDES";
+
+    private static final TextEncryptor NO_OP_TEXT_INSTANCE = new NoOpTextEncryptor();
+
+    private static final class NoOpTextEncryptor implements TextEncryptor {
+
+        public String encrypt(String text) {
+            return text;
+        }
+
+        public String decrypt(String encryptedText) {
+            return encryptedText;
+        }
+
+    }
+
+}

+ 45 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/HexEncodingTextEncryptor.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.encrypt;
+
+import static org.springframework.security.crypto.util.EncodingUtils.hexDecode;
+import static org.springframework.security.crypto.util.EncodingUtils.hexEncode;
+import static org.springframework.security.crypto.util.EncodingUtils.utf8Decode;
+import static org.springframework.security.crypto.util.EncodingUtils.utf8Encode;
+
+/**
+ * Delegates to an {@link BytesEncryptor} to encrypt text strings.
+ * Raw text strings are UTF-8 encoded before being passed to the encryptor.
+ * Encrypted strings are returned hex-encoded.
+ * @author Keith Donald
+ */
+final class HexEncodingTextEncryptor implements TextEncryptor {
+
+    private final BytesEncryptor encryptor;
+
+    public HexEncodingTextEncryptor(BytesEncryptor encryptor) {
+        this.encryptor = encryptor;
+    }
+
+    public String encrypt(String text) {
+        return hexEncode(encryptor.encrypt(utf8Encode(text)));
+    }
+
+    public String decrypt(String encryptedText) {
+        return utf8Decode(encryptor.decrypt(hexDecode(encryptedText)));
+    }
+
+}

+ 84 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/PasswordBasedBytesEncryptor.java

@@ -0,0 +1,84 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.encrypt;
+
+import static org.springframework.security.crypto.util.CipherUtils.doFinal;
+import static org.springframework.security.crypto.util.CipherUtils.initCipher;
+import static org.springframework.security.crypto.util.CipherUtils.newCipher;
+import static org.springframework.security.crypto.util.CipherUtils.newSecretKey;
+import static org.springframework.security.crypto.util.EncodingUtils.concatenate;
+import static org.springframework.security.crypto.util.EncodingUtils.subArray;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+
+import org.springframework.security.crypto.keygen.BytesKeyGenerator;
+import org.springframework.security.crypto.keygen.KeyGenerators;
+
+/**
+ * A general purpose encryptor for password-based encryption (PBEwith{prf}and{encryption} algorithms).
+ * Prepends a random salt to each encrypted value to aid in the prevention of password compromise with the aid of a dictionary/rainbow table.
+ * The salt allows the same secret key to be used for multiple encryption operations.
+ * The password should be not be shared.
+ * Note: {prf} = Pseudo random function e.g. MD5; {encryption} = Encryption method e.g. DES or AES.
+ * @author Keith Donald
+ */
+final class PasswordBasedBytesEncryptor implements BytesEncryptor {
+
+    private final SecretKey secretKey;
+
+    private final BytesKeyGenerator saltGenerator;
+
+    private final Cipher encryptor;
+
+    private final Cipher decryptor;
+
+    public PasswordBasedBytesEncryptor(String algorithm, String password) {
+        secretKey = newSecretKey(algorithm, password);
+        saltGenerator = KeyGenerators.secureRandom();
+        encryptor = newCipher(algorithm);
+        decryptor = newCipher(algorithm);
+    }
+
+    public byte[] encrypt(byte[] bytes) {
+        byte[] salt = saltGenerator.generateKey();
+        byte[] encrypted;
+        synchronized (encryptor) {
+            initCipher(encryptor, Cipher.ENCRYPT_MODE, secretKey, salt, 1024);
+            encrypted = doFinal(encryptor, bytes);
+        }
+        return concatenate(salt, encrypted);
+    }
+
+    public byte[] decrypt(byte[] encryptedBytes) {
+        byte[] salt = saltPart(encryptedBytes);
+        byte[] decrypted;
+        synchronized (decryptor) {
+            initCipher(decryptor, Cipher.DECRYPT_MODE, secretKey, salt, 1024);
+            decrypted = doFinal(decryptor, cipherPart(encryptedBytes, salt));
+        }
+        return decrypted;
+    }
+
+    private byte[] saltPart(byte[] encrypted) {
+        return subArray(encrypted, 0, saltGenerator.getKeyLength());
+    }
+
+    private byte[] cipherPart(byte[] encrypted, byte[] salt) {
+        return subArray(encrypted, salt.length, encrypted.length);
+    }
+
+}

+ 60 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/QueryableTextEncryptor.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.encrypt;
+
+import static org.springframework.security.crypto.util.CipherUtils.doFinal;
+import static org.springframework.security.crypto.util.CipherUtils.initCipher;
+import static org.springframework.security.crypto.util.CipherUtils.newCipher;
+import static org.springframework.security.crypto.util.CipherUtils.newSecretKey;
+import static org.springframework.security.crypto.util.EncodingUtils.hexDecode;
+import static org.springframework.security.crypto.util.EncodingUtils.hexEncode;
+import static org.springframework.security.crypto.util.EncodingUtils.utf8Decode;
+import static org.springframework.security.crypto.util.EncodingUtils.utf8Encode;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+
+/**
+ * A text encryptor that applies password-based MD5 plus DES symmetric key encryption.
+ * Designed to be used to encrypt fields that are queryable; for example, an indexed field such as an OAuth apiKey.
+ * Requires a random site-global salt to protect against password dictionary attacks.
+ * Does not salt on each {@link #encrypt(String)} operation to allow the encrypted field to be queried.
+ * @author Keith Donald
+ */
+final class QueryableTextEncryptor implements TextEncryptor {
+
+    private final Cipher encryptor;
+
+    private final Cipher decryptor;
+
+    public QueryableTextEncryptor(String algorithm, String password, String salt) {
+        byte[] saltBytes = hexDecode(salt);
+        SecretKey secretKey = newSecretKey(algorithm, password);
+        encryptor = newCipher(algorithm);
+        initCipher(encryptor, Cipher.ENCRYPT_MODE, secretKey, saltBytes, 1000);
+        decryptor = newCipher(algorithm);
+        initCipher(decryptor, Cipher.DECRYPT_MODE, secretKey, saltBytes, 1000);
+    }
+
+    public String encrypt(String text) {
+        return hexEncode(doFinal(encryptor, utf8Encode(text)));
+    }
+
+    public String decrypt(String encryptedText) {
+        return utf8Decode(doFinal(decryptor, hexDecode(encryptedText)));
+    }
+
+}

+ 34 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/TextEncryptor.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.encrypt;
+
+/**
+ * Service interface for symmetric encryption of text strings.
+ * @author Keith Donald
+ */
+public interface TextEncryptor {
+
+    /**
+     * Encrypt the raw text string.
+     */
+    String encrypt(String text);
+
+    /**
+     * Decrypt the encrypted text string.
+     */
+    String decrypt(String encryptedText);
+
+}

+ 5 - 0
crypto/src/main/java/org/springframework/security/crypto/encrypt/package-info.java

@@ -0,0 +1,5 @@
+/**
+ * Symmetric-key data encryption.
+ */
+package org.springframework.security.crypto.encrypt;
+

+ 35 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/BytesKeyGenerator.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.keygen;
+
+/**
+ * A generator for unique byte array-based keys.
+ * @author Keith Donald
+ */
+public interface BytesKeyGenerator {
+
+    /**
+     * Get the length, in bytes, of keys created by this generator.
+     * Most unique keys are at least 8 bytes in length.
+     */
+    int getKeyLength();
+
+    /**
+     * Generate a new key.
+     */
+    byte[] generateKey();
+
+}

+ 37 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/HexEncodingStringKeyGenerator.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.keygen;
+
+import static org.springframework.security.crypto.util.EncodingUtils.hexEncode;
+
+/**
+ * A StringKeyGenerator that generates hex-encoded String keys.
+ * Delegates to a {@link BytesKeyGenerator} for the actual key generation.
+ * @author Keith Donald
+ */
+final class HexEncodingStringKeyGenerator implements StringKeyGenerator {
+
+    private final BytesKeyGenerator keyGenerator;
+
+    public HexEncodingStringKeyGenerator(BytesKeyGenerator keyGenerator) {
+        this.keyGenerator = keyGenerator;
+    }
+
+    public String generateKey() {
+        return hexEncode(keyGenerator.generateKey());
+    }
+
+}

+ 54 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/KeyGenerators.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.keygen;
+
+import java.security.SecureRandom;
+
+/**
+ * Factory for commonly used key generators.
+ * Public API for constructing a {@link BytesKeyGenerator} or {@link StringKeyGenerator}.
+ * @author Keith Donald
+ */
+public class KeyGenerators {
+
+    /**
+     * Create a {@link BytesKeyGenerator} that uses a {@link SecureRandom} to generate keys of 8 bytes in length.
+     */
+    public static BytesKeyGenerator secureRandom() {
+        return new SecureRandomBytesKeyGenerator();
+    }
+
+    /**
+     * Create a {@link BytesKeyGenerator} that uses a {@link SecureRandom} to generate keys of a custom length.
+     * @param keyLength the key length in bytes, e.g. 16, for a 16 byte key.
+     */
+    public static BytesKeyGenerator secureRandom(int keyLength) {
+        return new SecureRandomBytesKeyGenerator(keyLength);
+    }
+
+    /**
+     * Creates a {@link StringKeyGenerator} that hex-encodes {@link SecureRandom} keys of 8 bytes in length.
+     * The hex-encoded string is keyLength * 2 characters in length.
+     */
+    public static StringKeyGenerator string() {
+        return new HexEncodingStringKeyGenerator(secureRandom());
+    }
+
+    // internal helpers
+
+    private KeyGenerators() {
+    }
+}

+ 85 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/SecureRandomBytesKeyGenerator.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.keygen;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+
+/**
+ * A KeyGenerator that uses SecureRandom to generate byte array-based keys.
+ * Defaults to 8 byte keys produced by the SHA1PRNG algorithm developed by the Sun Provider.
+ * @author Keith Donald
+ */
+final class SecureRandomBytesKeyGenerator implements BytesKeyGenerator {
+
+    private final SecureRandom random;
+
+    private final int keyLength;
+
+    /**
+     * Creates a secure random key generator using the defaults.
+     */
+    public SecureRandomBytesKeyGenerator() {
+        this(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, DEFAULT_KEY_LENGTH);
+    }
+
+    /**
+     * Creates a secure random key generator with a custom key length.
+     */
+    public SecureRandomBytesKeyGenerator(int keyLength) {
+        this(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, keyLength);
+    }
+
+    public int getKeyLength() {
+        return keyLength;
+    }
+
+    public byte[] generateKey() {
+        byte[] bytes = new byte[keyLength];
+        random.nextBytes(bytes);
+        return bytes;
+    }
+
+    // internal helpers
+
+    /**
+     * Creates a secure random key generator that is fully customized.
+     */
+    private SecureRandomBytesKeyGenerator(String algorithm, String provider, int keyLength) {
+        this.random = createSecureRandom(algorithm, provider, keyLength);
+        this.keyLength = keyLength;
+    }
+
+    private SecureRandom createSecureRandom(String algorithm, String provider, int keyLength) {
+        try {
+            SecureRandom random = SecureRandom.getInstance(algorithm, provider);
+            random.setSeed(random.generateSeed(keyLength));
+            return random;
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException("Not a supported SecureRandom key generation algorithm", e);
+        } catch (NoSuchProviderException e) {
+            throw new IllegalArgumentException("Not a supported SecureRandom key provider", e);
+        }
+    }
+
+    private static final String DEFAULT_ALGORITHM = "SHA1PRNG";
+
+    private static final String DEFAULT_PROVIDER = "SUN";
+
+    private static final int DEFAULT_KEY_LENGTH = 8;
+
+}

+ 26 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/StringKeyGenerator.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.keygen;
+
+/**
+ * A generator for unique string keys.
+ * @author Keith Donald
+ */
+public interface StringKeyGenerator {
+
+    String generateKey();
+
+}

+ 6 - 0
crypto/src/main/java/org/springframework/security/crypto/keygen/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * Secure random key generators.
+ */
+package org.springframework.security.crypto.keygen;
+
+

+ 46 - 0
crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.password;
+
+/**
+ * A password encoder that does nothing.
+ * Useful for testing where working with plain text passwords may be preferred.
+ * @author Keith Donald
+ */
+public final class NoOpPasswordEncoder implements PasswordEncoder {
+
+    public String encode(String rawPassword) {
+        return rawPassword;
+    }
+
+    public boolean matches(String rawPassword, String encodedPassword) {
+        return rawPassword.equals(encodedPassword);
+    }
+
+    /**
+     * Get the singleton {@link NoOpPasswordEncoder}.
+     */
+    public static PasswordEncoder getInstance() {
+        return INSTANCE;
+    }
+
+    private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder();
+
+    private NoOpPasswordEncoder() {
+
+    }
+
+}

+ 40 - 0
crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.password;
+
+/**
+ * Service interface for encoding passwords.
+ * @author Keith Donald
+ */
+public interface PasswordEncoder {
+
+    /**
+     * Encode the raw password.
+     * Generally, a good encoding algorithm applies a SHA-1 or greater hash combined with a 8-byte or greater randomly generated salt.
+     */
+    String encode(String rawPassword);
+
+    /**
+     * Verify the encoded password obtained from storage matches the submitted raw password after it too is encoded.
+     * Returns true if the passwords match, false if they do not.
+     * The stored password itself is never decoded.
+     * @param rawPassword the raw password to encode and match
+     * @param encodedPassword the encoded password from storage to compare with
+     * @return true if the raw password, after encoding, matches the encoded password from storage
+     */
+    boolean matches(String rawPassword, String encodedPassword);
+
+}

+ 86 - 0
crypto/src/main/java/org/springframework/security/crypto/password/StandardPasswordEncoder.java

@@ -0,0 +1,86 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.password;
+
+import static org.springframework.security.crypto.util.EncodingUtils.concatenate;
+import static org.springframework.security.crypto.util.EncodingUtils.hexDecode;
+import static org.springframework.security.crypto.util.EncodingUtils.hexEncode;
+import static org.springframework.security.crypto.util.EncodingUtils.subArray;
+import static org.springframework.security.crypto.util.EncodingUtils.utf8Encode;
+
+import java.util.Arrays;
+
+import org.springframework.security.crypto.keygen.BytesKeyGenerator;
+import org.springframework.security.crypto.keygen.KeyGenerators;
+import org.springframework.security.crypto.util.Digester;
+
+/**
+ * A standard PasswordEncoder implementation that uses SHA-256 1024 iteration hashing with 8-byte random salting.
+ * @author Keith Donald
+ */
+public final class StandardPasswordEncoder implements PasswordEncoder {
+
+    private final Digester digester;
+
+    private final byte[] secret;
+
+    private final BytesKeyGenerator saltGenerator;
+
+    /**
+     * Constructs a standard password encoder.
+     * @param secret the secret key used in the encoding process (should not be shared)
+     */
+    public StandardPasswordEncoder(String secret) {
+        this("SHA-256", "SUN", secret);
+    }
+
+    public String encode(String rawPassword) {
+        return encode(rawPassword, saltGenerator.generateKey());
+    }
+
+    public boolean matches(String rawPassword, String encodedPassword) {
+        byte[] digested = decode(encodedPassword);
+        byte[] salt = subArray(digested, 0, saltGenerator.getKeyLength());
+        return matches(digested, digest(rawPassword, salt));
+    }
+
+    // internal helpers
+
+    private StandardPasswordEncoder(String algorithm, String provider, String secret) {
+        this.digester = new Digester(algorithm, provider);
+        this.secret = utf8Encode(secret);
+        this.saltGenerator = KeyGenerators.secureRandom();
+    }
+
+    private String encode(String rawPassword, byte[] salt) {
+        byte[] digest = digest(rawPassword, salt);
+        return hexEncode(digest);
+    }
+
+    private byte[] digest(String rawPassword, byte[] salt) {
+        byte[] digest = digester.digest(concatenate(salt, secret, utf8Encode(rawPassword)));
+        return concatenate(salt, digest);
+    }
+
+    private byte[] decode(String encodedPassword) {
+        return hexDecode(encodedPassword);
+    }
+
+    private boolean matches(byte[] expected, byte[] actual) {
+        return Arrays.equals(expected, actual);
+    }
+
+}

+ 5 - 0
crypto/src/main/java/org/springframework/security/crypto/password/package-info.java

@@ -0,0 +1,5 @@
+/**
+ * Password encoders.
+ */
+package org.springframework.security.crypto.password;
+

+ 95 - 0
crypto/src/main/java/org/springframework/security/crypto/util/CipherUtils.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.util;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+
+/**
+ * Static helper for working with the Cipher API.
+ * @author Keith Donald
+ */
+public class CipherUtils {
+
+    /**
+     * Generates a SecretKey.
+     */
+    public static SecretKey newSecretKey(String algorithm, String secret) {
+        try {
+            PBEKeySpec pbeKeySpec = new PBEKeySpec(secret.toCharArray());
+            SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm);
+            return factory.generateSecret(pbeKeySpec);
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException("Not a valid encryption algorithm", e);
+        } catch (InvalidKeySpecException e) {
+            throw new IllegalArgumentException("Not a valid secert key", e);
+        }
+    }
+
+    /**
+     * Constructs a new Cipher.
+     */
+    public static Cipher newCipher(String algorithm) {
+        try {
+            return Cipher.getInstance(algorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException("Not a valid encryption algorithm", e);
+        } catch (NoSuchPaddingException e) {
+            throw new IllegalStateException("Should not happen", e);
+        }
+    }
+
+    /**
+     * Initializes the Cipher for use.
+     */
+    public static void initCipher(Cipher cipher, int mode, SecretKey secretKey, byte[] salt, int iterationCount) {
+        try {
+            cipher.init(mode, secretKey, new PBEParameterSpec(salt, iterationCount));
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("Unable to initialize due to invalid secret key", e);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new IllegalStateException("Unable to initialize due to invalid decryption parameter spec", e);
+        }
+    }
+
+    /**
+     * Invokes the Cipher to perform encryption or decryption (depending on the initialized mode).
+     */
+    public static byte[] doFinal(Cipher cipher, byte[] input) {
+        try {
+            return cipher.doFinal(input);
+        } catch (IllegalBlockSizeException e) {
+            throw new IllegalStateException("Unable to invoke Cipher due to illegal block size", e);
+        } catch (BadPaddingException e) {
+            throw new IllegalStateException("Unable to invoke Cipher due to bad padding", e);
+        }
+    }
+
+    private CipherUtils() {
+    }
+
+}

+ 62 - 0
crypto/src/main/java/org/springframework/security/crypto/util/Digester.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.util;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+
+/**
+ * Helper for working with the MessageDigest API.
+ * Performs 1024 iterations of the hashing algorithm per digest to aid in protecting against brute force attacks.
+ * @author Keith Donald
+ */
+public class Digester {
+
+    private final MessageDigest messageDigest;
+
+    private final int iterations = 1024;
+
+    /**
+     * Create a new Digester.
+     * @param algorithm the digest algorithm; for example, "SHA-1" or "SHA-256".
+     * @param provider the provider of the digest algorithm, for example "SUN".
+     */
+    public Digester(String algorithm, String provider) {
+        try {
+            messageDigest = MessageDigest.getInstance(algorithm, provider);
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException("No such hashing algorithm", e);
+        } catch (NoSuchProviderException e) {
+            throw new IllegalStateException("No such provider for hashing algorithm", e);
+        }
+    }
+
+    public byte[] digest(byte[] value) {
+        synchronized (messageDigest) {
+            for (int i = 0; i < (iterations - 1); i++) {
+                invokeDigest(value);
+            }
+            return messageDigest.digest(value);
+        }
+    }
+
+    private byte[] invokeDigest(byte[] value) {
+        messageDigest.reset();
+        return messageDigest.digest(value);
+    }
+
+}

+ 122 - 0
crypto/src/main/java/org/springframework/security/crypto/util/EncodingUtils.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * 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.springframework.security.crypto.util;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Static helper for encoding data.
+ * @author Keith Donald
+ */
+public class EncodingUtils {
+
+    /**
+     * Encode the byte array into a hex String.
+     */
+    public static String hexEncode(byte[] bytes) {
+        StringBuilder result = new StringBuilder();
+        char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+        for (int i = 0; i < bytes.length; ++i) {
+            byte b = bytes[i];
+            result.append(digits[(b & 0xf0) >> 4]);
+            result.append(digits[b & 0x0f]);
+        }
+        return result.toString();
+    }
+
+    /**
+     * Decode the hex String into a byte array.
+     */
+    public static byte[] hexDecode(String s) {
+        int len = s.length();
+        byte[] r = new byte[len / 2];
+        for (int i = 0; i < r.length; i++) {
+            int digit1 = s.charAt(i * 2), digit2 = s.charAt(i * 2 + 1);
+            if ((digit1 >= '0') && (digit1 <= '9'))  {
+                digit1 -= '0';
+            } else if ((digit1 >= 'a') && (digit1 <= 'f')) {
+                digit1 -= 'a' - 10;
+            }
+            if ((digit2 >= '0') && (digit2 <= '9')) {
+                digit2 -= '0';
+            } else if ((digit2 >= 'a') && (digit2 <= 'f')) {
+                digit2 -= 'a' - 10;
+            }
+            r[i] = (byte) ((digit1 << 4) + digit2);
+        }
+        return r;
+    }
+
+    /**
+     * Get the bytes of the String in UTF-8 encoded form.
+     */
+    public static byte[] utf8Encode(String string) {
+        try {
+            return string.getBytes("UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw encodingException(e);
+        }
+    }
+
+    /**
+     * Decode the bytes in UTF-8 form into a String.
+     */
+    public static String utf8Decode(byte[] bytes) {
+        try {
+            return new String(bytes, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw encodingException(e);
+        }
+    }
+
+    /**
+     * Combine the individual byte arrays into one array.
+     */
+    public static byte[] concatenate(byte[]... arrays) {
+        int length = 0;
+        for (byte[] array : arrays) {
+            length += array.length;
+        }
+        byte[] newArray = new byte[length];
+        int destPos = 0;
+        for (byte[] array : arrays) {
+            System.arraycopy(array, 0, newArray, destPos, array.length);
+            destPos += array.length;
+        }
+        return newArray;
+    }
+
+    /**
+     * Extract a sub array of bytes out of the byte array.
+     * @param array the byte array to extract from
+     * @param beginIndex the beginning index of the sub array, inclusive
+     * @param endIndex the ending index of the sub array, exclusive
+     */
+    public static byte[] subArray(byte[] array, int beginIndex, int endIndex) {
+        int length = endIndex - beginIndex;
+        byte[] subarray = new byte[length];
+        System.arraycopy(array, beginIndex, subarray, 0, length);
+        return subarray;
+    }
+
+    private EncodingUtils() {
+    }
+
+    private static RuntimeException encodingException(UnsupportedEncodingException e) {
+        return new IllegalStateException("UTF-8 is not an available char set", e);
+    }
+
+}

+ 4 - 0
crypto/src/main/java/org/springframework/security/crypto/util/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * Shared crypto utilities.
+ */
+package org.springframework.security.crypto.util;

+ 49 - 0
crypto/src/test/java/org/springframework/security/crypto/encrypt/EncryptorsTests.java

@@ -0,0 +1,49 @@
+package org.springframework.security.crypto.encrypt;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class EncryptorsTests {
+
+    @Test
+    public void standard() {
+        BytesEncryptor encryptor = Encryptors.standard("password");
+        byte[] result = encryptor.encrypt("text".getBytes());
+        assertNotNull(result);
+        assertFalse(new String(result).equals("text"));
+        assertEquals("text", new String(encryptor.decrypt(result)));
+        assertFalse(new String(result).equals(new String(encryptor.encrypt("text".getBytes()))));
+    }
+
+    @Test
+    public void text() {
+        TextEncryptor encryptor = Encryptors.text("password");
+        String result = encryptor.encrypt("text");
+        assertNotNull(result);
+        assertFalse(result.equals("text"));
+        assertEquals("text", encryptor.decrypt(result));
+        assertFalse(result.equals(encryptor.encrypt("text")));
+    }
+
+    @Test
+    public void queryableText() {
+        TextEncryptor encryptor = Encryptors.queryableText("password", "5c0744940b5c369b");
+        String result = encryptor.encrypt("text");
+        assertNotNull(result);
+        assertFalse(result.equals("text"));
+        assertEquals("text", encryptor.decrypt(result));
+        assertTrue(result.equals(encryptor.encrypt("text")));
+    }
+
+    @Test
+    public void noOpText() {
+        TextEncryptor encryptor = Encryptors.noOpText();
+        assertEquals("text", encryptor.encrypt("text"));
+        assertEquals("text", encryptor.decrypt("text"));
+    }
+
+}

+ 43 - 0
crypto/src/test/java/org/springframework/security/crypto/keygen/KeyGeneratorsTests.java

@@ -0,0 +1,43 @@
+package org.springframework.security.crypto.keygen;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.springframework.security.crypto.util.EncodingUtils;
+
+public class KeyGeneratorsTests {
+
+    @Test
+    public void secureRandom() {
+        BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
+        assertEquals(8, keyGenerator.getKeyLength());
+        byte[] key = keyGenerator.generateKey();
+        assertEquals(8, key.length);
+        byte[] key2 = keyGenerator.generateKey();
+        assertFalse(Arrays.equals(key, key2));
+    }
+
+    @Test
+    public void secureRandomCustomLength() {
+        BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(16);
+        assertEquals(16, keyGenerator.getKeyLength());
+        byte[] key = keyGenerator.generateKey();
+        assertEquals(16, key.length);
+        byte[] key2 = keyGenerator.generateKey();
+        assertFalse(Arrays.equals(key, key2));
+    }
+
+    @Test
+    public void string() {
+        StringKeyGenerator keyGenerator = KeyGenerators.string();
+        String hexStringKey = keyGenerator.generateKey();
+        assertEquals(16, hexStringKey.length());
+        assertEquals(8, EncodingUtils.hexDecode(hexStringKey).length);
+        String hexStringKey2 = keyGenerator.generateKey();
+        assertFalse(hexStringKey.equals(hexStringKey2));
+    }
+
+}

+ 25 - 0
crypto/src/test/java/org/springframework/security/crypto/password/StandardPasswordEncoderTests.java

@@ -0,0 +1,25 @@
+package org.springframework.security.crypto.password;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class StandardPasswordEncoderTests {
+
+    private StandardPasswordEncoder encoder = new StandardPasswordEncoder("secret");
+
+    @Test
+    public void matches() {
+        String result = encoder.encode("password");
+        assertFalse(result.equals("password"));
+        assertTrue(encoder.matches("password", result));
+    }
+
+    @Test
+    public void notMatches() {
+        String result = encoder.encode("password");
+        assertFalse(encoder.matches("bogus", result));
+    }
+
+}

+ 19 - 0
crypto/src/test/java/org/springframework/security/crypto/util/DigesterTests.java

@@ -0,0 +1,19 @@
+package org.springframework.security.crypto.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.Test;
+
+public class DigesterTests {
+
+    private Digester digester = new Digester("SHA-1", "SUN");
+
+    @Test
+    public void digest() {
+        byte[] result = digester.digest("text".getBytes());
+        assertEquals(20, result.length);
+        assertFalse(new String(result).equals("text"));
+    }
+
+}

+ 44 - 0
crypto/src/test/java/org/springframework/security/crypto/util/EncodingUtilsTests.java

@@ -0,0 +1,44 @@
+package org.springframework.security.crypto.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+public class EncodingUtilsTests {
+
+    @Test
+    public void hexEncode() {
+        byte[] bytes = new byte[] { (byte)0x01, (byte)0xFF, (byte)65, (byte)66, (byte)67, (byte)0xC0, (byte)0xC1, (byte)0xC2 };
+        String result = EncodingUtils.hexEncode(bytes);
+        assertEquals("01ff414243c0c1c2", result);
+    }
+
+    @Test
+    public void hexDecode() {
+        byte[] bytes = new byte[] { (byte)0x01, (byte)0xFF, (byte)65, (byte)66, (byte)67, (byte)0xC0, (byte)0xC1, (byte)0xC2 };
+        byte[] result = EncodingUtils.hexDecode("01ff414243c0c1c2");
+        assertTrue(Arrays.equals(bytes, result));
+    }
+
+    @Test
+    public void concatenate() {
+        byte[] bytes = new byte[] { (byte)0x01, (byte)0xFF, (byte)65, (byte)66, (byte)67, (byte)0xC0, (byte)0xC1, (byte)0xC2 };
+        byte[] one = new byte[] { (byte)0x01 };
+        byte[] two = new byte[] { (byte)0xFF, (byte)65, (byte)66 };
+        byte[] three = new byte[] { (byte)67, (byte)0xC0, (byte)0xC1, (byte)0xC2 };
+        assertTrue(Arrays.equals(bytes, EncodingUtils.concatenate(one, two, three)));
+    }
+
+    @Test
+    public void subArray() {
+        byte[] bytes = new byte[] { (byte)0x01, (byte)0xFF, (byte)65, (byte)66, (byte)67, (byte)0xC0, (byte)0xC1, (byte)0xC2 };
+        byte[] two = new byte[] { (byte)0xFF, (byte)65, (byte)66 };
+        byte[] subArray = EncodingUtils.subArray(bytes, 1, 4);
+        assertEquals(3, subArray.length);
+        assertTrue(Arrays.equals(two, subArray));
+    }
+
+}

+ 16 - 0
crypto/template.mf

@@ -0,0 +1,16 @@
+Implementation-Title: org.springframework.security.crypto
+Implementation-Version: ${version}
+Bundle-SymbolicName: org.springframework.security.crypto
+Bundle-Name: Spring Security Web
+Bundle-Vendor: SpringSource
+Bundle-Version: ${version}
+Bundle-ManifestVersion: 2
+Excluded-Imports:
+ javax.naming.*,
+ javax.rmi.*,
+ javax.sql.*,
+ javax.security.auth.*
+Ignored-Existing-Headers:
+ Import-Package,
+ Export-Package
+Import-Template:

+ 2 - 1
settings.gradle

@@ -7,7 +7,8 @@ def String[] modules = [
     'cas',
     'openid',
     'taglibs',
-    'aspects'
+    'aspects',
+    'crypto'
 ]
 
 def String[] samples = [