View Javadoc

1   /**
2    * This file Copyright (c) 2003-2013 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.security;
35  
36  import info.magnolia.cms.core.Path;
37  import info.magnolia.cms.core.SystemProperty;
38  import info.magnolia.cms.exchange.ActivationManager;
39  import info.magnolia.cms.security.auth.ACL;
40  import info.magnolia.cms.security.auth.PrincipalCollectionImpl;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.objectfactory.Components;
43  
44  import java.io.File;
45  import java.io.FileInputStream;
46  import java.io.FileNotFoundException;
47  import java.io.FileWriter;
48  import java.io.IOException;
49  import java.io.InputStream;
50  import java.io.OutputStream;
51  import java.security.DigestInputStream;
52  import java.security.DigestOutputStream;
53  import java.security.InvalidKeyException;
54  import java.security.KeyFactory;
55  import java.security.KeyPair;
56  import java.security.KeyPairGenerator;
57  import java.security.MessageDigest;
58  import java.security.NoSuchAlgorithmException;
59  import java.security.NoSuchProviderException;
60  import java.security.Principal;
61  import java.security.PrivateKey;
62  import java.security.PublicKey;
63  import java.security.Security;
64  import java.security.spec.InvalidKeySpecException;
65  import java.security.spec.PKCS8EncodedKeySpec;
66  import java.security.spec.X509EncodedKeySpec;
67  import java.text.SimpleDateFormat;
68  import java.util.ArrayList;
69  import java.util.Collection;
70  import java.util.Date;
71  import java.util.HashSet;
72  import java.util.List;
73  import java.util.Properties;
74  
75  import javax.crypto.BadPaddingException;
76  import javax.crypto.Cipher;
77  import javax.crypto.IllegalBlockSizeException;
78  import javax.crypto.NoSuchPaddingException;
79  import javax.jcr.RepositoryException;
80  import javax.jcr.Session;
81  import javax.security.auth.Subject;
82  
83  import org.apache.commons.lang.StringUtils;
84  import org.bouncycastle.jce.provider.BouncyCastleProvider;
85  import org.mindrot.jbcrypt.BCrypt;
86  import org.slf4j.Logger;
87  import org.slf4j.LoggerFactory;
88  
89  /**
90   * Utility functions required in the context of Security.
91   * 
92   * @version $Id$
93   */
94  public class SecurityUtil {
95  
96      private static final String PRIVATE_KEY = "key.private";
97      private static final String PUBLIC_KEY = "key.public";
98      private static final String KEY_LOCATION_PROPERTY = "magnolia.author.key.location";
99  
100     public static final String SHA1 = "SHA-1"; //$NON-NLS-1$
101     public static final String MD5 = "MD5"; //$NON-NLS-1$
102 
103     /**
104      * There are five (5) FIPS-approved* algorithms for generating a condensed representation of a message (message
105      * digest): SHA-1, SHA-224, SHA-256,SHA-384, and SHA-512. <strong>Not supported yet</strong>
106      */
107     public static final String SHA256 = "SHA-256"; //$NON-NLS-1$
108     public static final String SHA384 = "SHA-384"; //$NON-NLS-1$
109     public static final String SHA512 = "SHA-512"; //$NON-NLS-1$
110 
111     /**
112      * Encryption algorithm used ... if you are ever changing this, keep in mind underlying impl relies on padding!
113      */
114 
115     private static final String ALGORITHM = "RSA";
116 
117     private static Logger log = LoggerFactory.getLogger(SecurityUtil.class);
118 
119     static {
120         Security.addProvider(new BouncyCastleProvider());
121     }
122 
123     /**
124      * Checks if the currently acting user is anonymous.
125      */
126     public static boolean isAnonymous() {
127         User user = MgnlContext.getUser();
128         return user != null && UserManager.ANONYMOUS_USER.equals(user.getName());
129     }
130 
131     public static boolean isAuthenticated() {
132         User user = MgnlContext.getUser();
133         return user != null && !UserManager.ANONYMOUS_USER.equals(user.getName());
134     }
135 
136     public static String decrypt(String pass) throws SecurityException {
137         return decrypt(pass, getPublicKey());
138     }
139 
140     public static String decrypt(String message, String encodedKey) throws SecurityException {
141         try {
142             if (StringUtils.isBlank(encodedKey)) {
143                 throw new SecurityException("Activation key was not found. Please make sure your instance is correctly configured.");
144             }
145 
146             // decode key
147             byte[] binaryKey = hexToByteArray(encodedKey);
148 
149             // create RSA public key cipher
150             Cipher pkCipher = Cipher.getInstance(ALGORITHM, "BC");
151             try {
152                 // create private key
153                 X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(binaryKey);
154                 KeyFactory kf = KeyFactory.getInstance(ALGORITHM, "BC");
155                 PublicKey pk = kf.generatePublic(publicKeySpec);
156                 pkCipher.init(Cipher.DECRYPT_MODE, pk);
157 
158             } catch (InvalidKeySpecException e) {
159                 // decrypting with private key?
160                 PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(binaryKey);
161                 KeyFactory kf = KeyFactory.getInstance(ALGORITHM, "BC");
162                 PrivateKey pk = kf.generatePrivate(privateKeySpec);
163                 pkCipher.init(Cipher.DECRYPT_MODE, pk);
164             }
165 
166             // decrypt
167             String[] chunks = StringUtils.split(message, ";");
168             if (chunks == null) {
169                 throw new SecurityException("The encrypted information is corrupted or incomplete. Please make sure someone is not trying to intercept or modify encrypted message.");
170             }
171             StringBuilder clearText = new StringBuilder();
172             for (String chunk : chunks) {
173                 byte[] byteChunk = hexToByteArray(chunk);
174                 clearText.append(new String(pkCipher.doFinal(byteChunk), "UTF-8"));
175             }
176             return clearText.toString();
177         } catch (NumberFormatException e) {
178             throw new SecurityException("The encrypted information is corrupted or incomplete. Please make sure someone is not trying to intercept or modify encrypted message.", e);
179         } catch (IOException e) {
180             throw new SecurityException("Failed to read authentication string. Please use Java version with cryptography support.", e);
181         } catch (NoSuchAlgorithmException e) {
182             throw new SecurityException("Failed to read authentication string. Please use Java version with cryptography support.", e);
183         } catch (NoSuchPaddingException e) {
184             throw new SecurityException("Failed to read authentication string. Please use Java version with cryptography support.", e);
185         } catch (InvalidKeySpecException e) {
186             throw new SecurityException("Failed to read authentication string. Please use Java version with cryptography support.", e);
187         } catch (InvalidKeyException e) {
188             throw new SecurityException("Failed to read authentication string. Please use Java version with cryptography support.", e);
189         } catch (NoSuchProviderException e) {
190             throw new SecurityException("Failed to find encryption provider. Please use Java version with cryptography support.", e);
191         } catch (IllegalBlockSizeException e) {
192             throw new SecurityException("Failed to decrypt message. It might have been corrupted during transport.", e);
193         } catch (BadPaddingException e) {
194             throw new SecurityException("Failed to decrypt message. It might have been corrupted during transport.", e);
195         }
196 
197     }
198 
199     public static String encrypt(String pass) throws SecurityException {
200         String encodedKey = getPrivateKey();
201         return encrypt(pass, encodedKey);
202     }
203 
204     public static String encrypt(String message, String encodedKey) {
205         try {
206 
207             // read private key
208             if (StringUtils.isBlank(encodedKey)) {
209                 throw new SecurityException("Activation key was not found. Please make sure your instance is correctly configured.");
210             }
211             byte[] binaryKey = hexToByteArray(encodedKey);
212 
213             // create RSA public key cipher
214             Cipher pkCipher = Cipher.getInstance(ALGORITHM, "BC");
215             try {
216                 // create private key
217                 PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(binaryKey);
218                 KeyFactory kf = KeyFactory.getInstance(ALGORITHM, "BC");
219                 PrivateKey pk = kf.generatePrivate(privateKeySpec);
220 
221                 pkCipher.init(Cipher.ENCRYPT_MODE, pk);
222             } catch (InvalidKeySpecException e) {
223                 // encrypting with public key?
224                 X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(binaryKey);
225                 KeyFactory kf = KeyFactory.getInstance(ALGORITHM, "BC");
226                 PublicKey pk = kf.generatePublic(publicKeySpec);
227 
228                 pkCipher.init(Cipher.ENCRYPT_MODE, pk);
229             }
230 
231             // encrypt
232             byte[] bytes = message.getBytes("UTF-8");
233             // split bit message in chunks
234             int start = 0;
235             StringBuilder chaos = new StringBuilder();
236             while (start < bytes.length) {
237                 byte[] tmp = new byte[Math.min(bytes.length - start, binaryKey.length / 8)];
238                 System.arraycopy(bytes, start, tmp, 0, tmp.length);
239                 start += tmp.length;
240                 byte[] encrypted = pkCipher.doFinal(tmp);
241                 chaos.append(byteArrayToHex(encrypted));
242                 chaos.append(";");
243             }
244             chaos.setLength(chaos.length() - 1);
245 
246             return chaos.toString();
247 
248         } catch (IOException e) {
249             throw new SecurityException("Failed to create authentication string. Please use Java version with cryptography support.", e);
250         } catch (NoSuchAlgorithmException e) {
251             throw new SecurityException("Failed to create authentication string. Please use Java version with cryptography support.", e);
252         } catch (NoSuchPaddingException e) {
253             throw new SecurityException("Failed to create authentication string. Please use Java version with cryptography support.", e);
254         } catch (InvalidKeySpecException e) {
255             throw new SecurityException("Failed to create authentication string. Please use Java version with cryptography support.", e);
256         } catch (InvalidKeyException e) {
257             throw new SecurityException("Failed to create authentication string. Please use Java version with cryptography support.", e);
258         } catch (NoSuchProviderException e) {
259             throw new SecurityException("Failed to find encryption provider. Please use Java version with cryptography support.", e);
260         } catch (IllegalBlockSizeException e) {
261             throw new SecurityException("Failed to encrypt string. Please use Java version with cryptography support.", e);
262         } catch (BadPaddingException e) {
263             throw new SecurityException("Failed to encrypt string. Please use Java version with cryptography support.", e);
264         }
265     }
266 
267     public static String getPrivateKey() {
268         String path = SystemProperty.getProperty(KEY_LOCATION_PROPERTY);
269         checkPrivateKeyStoreExistence(path);
270         try {
271             Properties defaultProps = new Properties();
272             FileInputStream in = new FileInputStream(path);
273             defaultProps.load(in);
274             in.close();
275             return defaultProps.getProperty(PRIVATE_KEY);
276         } catch (FileNotFoundException e) {
277             throw new SecurityException("Failed to retrieve private key. Please make sure the key is located in " + path, e);
278         } catch (IOException e) {
279             throw new SecurityException("Failed to retrieve private key. Please make sure the key is located in " + path, e);
280         }
281     }
282 
283     public static void updateKeys(MgnlKeyPair keys) throws IllegalArgumentException {
284         // update filestore only when private key is present
285         if (keys.getPrivateKey() != null) {
286             String path = SystemProperty.getProperty(KEY_LOCATION_PROPERTY);
287             if (path == null) {
288                 final String errorMsg = KEY_LOCATION_PROPERTY + " is not specified. Please set the location of the key store file in your 'magnolia.properties'.";
289                 log.error(errorMsg);
290                 throw new IllegalStateException(errorMsg);
291             }
292             try {
293                 Properties defaultProps = new Properties();
294                 defaultProps.put(PRIVATE_KEY, keys.getPrivateKey());
295                 defaultProps.put(PUBLIC_KEY, keys.getPublicKey());
296                 File keystore = new File(path);
297                 File parentFile = keystore.getParentFile();
298                 if (parentFile != null) {
299                     parentFile.mkdirs();
300                 }
301                 FileWriter writer = new FileWriter(keystore);
302                 String date = new SimpleDateFormat("dd.MMM.yyyy hh:mm").format(new Date());
303                 defaultProps.store(writer, "generated " + date + " by " + MgnlContext.getUser().getName());
304                 writer.close();
305             } catch (FileNotFoundException e) {
306                 throw new SecurityException("Failed to store private key. Please make sure the key is located in " + path, e);
307             } catch (IOException e) {
308                 throw new SecurityException("Failed to store private key. Please make sure the key is located in " + path, e);
309             }
310         }
311         try {
312             Session session = MgnlContext.getSystemContext().getJCRSession("config");
313             session.getNode("/server/activation").setProperty("publicKey", keys.getPublicKey());
314             session.save();
315         } catch (RepositoryException e) {
316             throw new SecurityException("Failed to store public key.", e);
317         }
318     }
319 
320     public static String getPublicKey() {
321         ActivationManager aman = Components.getComponentProvider().getComponent(ActivationManager.class);
322         return aman.getPublicKey();
323     }
324 
325     private static final String HEX = "0123456789ABCDEF";
326 
327     public static String byteArrayToHex(byte[] raw) {
328         if (raw == null) {
329             return null;
330         }
331         final StringBuilder hex = new StringBuilder(2 * raw.length);
332         for (final byte b : raw) {
333             hex.append(HEX.charAt((b & 0xF0) >> 4))
334                     .append(HEX.charAt(b & 0x0F));
335         }
336         return hex.toString();
337     }
338 
339     public static byte[] hexToByteArray(String s) {
340         byte[] b = new byte[s.length() / 2];
341         for (int i = 0; i < b.length; i++) {
342             int index = i * 2;
343             int v = Integer.parseInt(s.substring(index, index + 2), 16);
344             b[i] = (byte) v;
345         }
346         return b;
347     }
348 
349     public static MgnlKeyPair generateKeyPair(int keyLength) throws NoSuchAlgorithmException {
350         KeyPairGenerator kgen = KeyPairGenerator.getInstance(ALGORITHM);
351         kgen.initialize(keyLength);
352         KeyPair key = kgen.genKeyPair();
353         return new MgnlKeyPair(byteArrayToHex(key.getPrivate().getEncoded()), byteArrayToHex(key.getPublic().getEncoded()));
354     }
355 
356     /**
357      * Used for removing password parameter from cache key.
358      * 
359      * @param cacheKey.toString()
360      * @return
361      */
362     public static String stripPasswordFromCacheLog(String log) {
363         String value = stripParameterFromCacheLog(log, "mgnlUserPSWD");
364         value = stripParameterFromCacheLog(value, "passwordConfirmation");
365         value = stripParameterFromCacheLog(value, "password");
366         return value;
367     }
368 
369     public static String stripPasswordFromUrl(String url) {
370         if (StringUtils.isBlank(url)) {
371             return null;
372         }
373         String value = null;
374         value = StringUtils.substringBefore(url, "mgnlUserPSWD");
375         value = value + StringUtils.substringAfter(StringUtils.substringAfter(url, "mgnlUserPSWD"), "&");
376         return StringUtils.removeEnd(value, "&");
377     }
378 
379     public static String stripParameterFromCacheLog(String log, String parameter) {
380         if (StringUtils.isBlank(log)) {
381             return null;
382         } else if (!StringUtils.contains(log, parameter)) {
383             return log;
384         }
385         String value = null;
386         value = StringUtils.substringBefore(log, parameter);
387         String afterString = StringUtils.substringAfter(log, parameter);
388         if (StringUtils.indexOf(afterString, " ") < StringUtils.indexOf(afterString, "}")) {
389             value = value + StringUtils.substringAfter(afterString, " ");
390         } else {
391             value = value + "}" + StringUtils.substringAfter(afterString, "}");
392         }
393         return value;
394     }
395 
396     private static void checkPrivateKeyStoreExistence(final String path) throws SecurityException {
397         if (StringUtils.isBlank(path)) {
398             throw new SecurityException("Private key store path is either null or empty. Please, check [" + KEY_LOCATION_PROPERTY + "] value in magnolia.properties");
399         }
400         String absPath = Path.getAbsoluteFileSystemPath(path);
401         File keypair = new File(absPath);
402         if (!keypair.exists()) {
403             throw new SecurityException("Private key store doesn't exist at [" + keypair.getAbsolutePath() + "]. Please, ensure that [" + KEY_LOCATION_PROPERTY + "] actually points to the correct location");
404         }
405     }
406 
407     public static String getBCrypt(String text) {
408         // gensalt's log_rounds parameter determines the complexity
409         // the work factor is 2^log_rounds, and the default is 10
410         String hashed = BCrypt.hashpw(text, BCrypt.gensalt(12));
411         return hashed;
412     }
413 
414     public static boolean matchBCrypted(String candidate, String hash) {
415         // Check that an unencrypted password matches one that has
416         // previously been hashed
417         return BCrypt.checkpw(candidate, hash);
418     }
419 
420     /**
421      * Message Digesting function.
422      */
423     public static String getDigest(String data, String algorithm) throws NoSuchAlgorithmException {
424         MessageDigest md = MessageDigest.getInstance(algorithm);
425         md.reset();
426         return new String(md.digest(data.getBytes()));
427     }
428 
429     /**
430      * Message Digesting function.
431      */
432     public static byte[] getDigest(byte[] data, String algorithm) throws NoSuchAlgorithmException {
433         MessageDigest md = MessageDigest.getInstance(algorithm);
434         md.reset();
435         return md.digest(data);
436     }
437 
438     public static DigestInputStream getDigestInputStream(InputStream stream) {
439         MessageDigest md;
440         try {
441             md = MessageDigest.getInstance(SecurityUtil.MD5);
442             md.reset();
443             return new DigestInputStream(stream, md);
444         } catch (NoSuchAlgorithmException e) {
445             throw new SecurityException("Couldn't digest with " + SecurityUtil.MD5 + " algorithm!");
446         }
447     }
448 
449     public static DigestOutputStream getDigestOutputStream(OutputStream stream) {
450         MessageDigest md;
451         try {
452             md = MessageDigest.getInstance(SecurityUtil.MD5);
453             md.reset();
454             return new DigestOutputStream(stream, md);
455         } catch (NoSuchAlgorithmException e) {
456             throw new SecurityException("Couldn't digest with " + SecurityUtil.MD5 + " algorithm!");
457         }
458     }
459 
460     /**
461      * Gets SHA-1 encoded -> hex string.
462      */
463     public static String getSHA1Hex(byte[] data) {
464         try {
465             return byteArrayToHex(getDigest(data, SecurityUtil.SHA1));
466         } catch (NoSuchAlgorithmException e) {
467             throw new SecurityException("Couldn't digest with " + SecurityUtil.SHA1 + " algorithm!");
468         }
469     }
470 
471     public static String getSHA1Hex(String data) {
472         return getSHA1Hex(data.getBytes());
473     }
474 
475     /**
476      * Gets MD5 encoded -> hex string.
477      */
478     public static String getMD5Hex(byte[] data) {
479         try {
480             return byteArrayToHex(getDigest(data, SecurityUtil.MD5));
481         } catch (NoSuchAlgorithmException e) {
482             throw new SecurityException("Couldn't digest with " + SecurityUtil.MD5 + " algorithm!");
483         }
484     }
485 
486     public static String getMD5Hex(String data) {
487         return getMD5Hex(data.getBytes());
488     }
489 
490     public static String getMD5Hex(DigestInputStream stream) {
491         return byteArrayToHex(stream.getMessageDigest().digest());
492     }
493 
494     public static String getMD5Hex(DigestOutputStream stream) {
495         return byteArrayToHex(stream.getMessageDigest().digest());
496     }
497 
498     public static Subject createSubjectAndPopulate(User user) {
499 
500         RoleManager roleManager = Components.getComponent(SecuritySupport.class).getRoleManager();
501 
502         List<Principal> acls = new ArrayList<Principal>();
503         for (String role : user.getAllRoles()) {
504             acls.addAll(roleManager.getACLs(role).values());
505         }
506 
507         PrincipalCollectionImpl principalCollection = new PrincipalCollectionImpl();
508         mergePrincipals(principalCollection, acls);
509 
510         Subject subject = new Subject();
511         subject.getPrincipals().add(user);
512         subject.getPrincipals().add(principalCollection);
513         return subject;
514     }
515 
516     private static void mergePrincipals(PrincipalCollectionImpl principalCollection, List<Principal> acls) {
517         for (Principal principal : acls) {
518             ACL princ = (ACL) principal;
519             if (principalCollection.contains(princ.getName())) {
520                 ACL oldACL = (ACL) principalCollection.get(princ.getName());
521                 Collection<Permission> permissions = new HashSet<Permission>(oldACL.getList());
522                 permissions.addAll(princ.getList());
523                 principalCollection.remove(oldACL);
524                 princ = new ACLImpl(princ.getName(), new ArrayList<Permission>(permissions));
525             }
526             principalCollection.add(princ);
527         }
528     }
529 
530 }