View Javadoc
1   /*
2    * Copyright 2012 Brian Matthews
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.btmatthews.maven.plugins.crx;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.File;
21  import java.io.FileReader;
22  import java.io.IOException;
23  import java.io.Reader;
24  import java.math.BigInteger;
25  import java.security.*;
26  import java.security.interfaces.RSAPrivateCrtKey;
27  import java.security.spec.InvalidKeySpecException;
28  import java.security.spec.RSAPublicKeySpec;
29  import java.util.zip.Deflater;
30  
31  import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
32  import org.bouncycastle.jce.provider.BouncyCastleProvider;
33  import org.bouncycastle.openssl.*;
34  import org.bouncycastle.openssl.bc.BcPEMDecryptorProvider;
35  import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
36  import org.codehaus.plexus.archiver.ArchiverException;
37  import org.codehaus.plexus.archiver.ResourceIterator;
38  import org.codehaus.plexus.archiver.zip.AbstractZipArchiver;
39  import org.codehaus.plexus.archiver.zip.ZipOutputStream;
40  import org.codehaus.plexus.component.annotations.Component;
41  import org.codehaus.plexus.component.annotations.Requirement;
42  
43  /**
44   * This archiver packages and signs a Google Chrome Extension.
45   *
46   * @author <a href="mailto:brian@btmatthews.com">Brian Matthews</a>
47   * @since 1.1.0
48   */
49  @Component(role = CRXArchiver.class, hint = "crx", instantiationStrategy = "per-lookup")
50  public class CRXArchiverImpl extends AbstractZipArchiver implements CRXArchiver {
51  
52      /**
53       * The location of the .pem file containing the public/private key pair.
54       */
55      private File pemFile;
56  
57      /**
58       * The password used to secure the .pem file.
59       */
60      private String pemPassword;
61  
62      /**
63       * The helper that is used to sign the ZIP archive.
64       */
65      @Requirement(hint = "crx")
66      private SignatureHelper signatureHelper;
67  
68      /**
69       * The helper that is used to output the CRX archive.
70       */
71      @Requirement(hint = "crx")
72      private ArchiveHelper archiveHelper;
73  
74      /**
75       * Used to inject the location of the .pem file containing the public/private key pair.
76       *
77       * @param file The location of the .pem file.
78       */
79      public void setPemFile(final File file) {
80          pemFile = file;
81      }
82  
83      /**
84       * Used to inject the password that was used to secure the .pem file.
85       *
86       * @param password The password.
87       */
88      public final void setPemPassword(final String password) {
89          pemPassword = password;
90      }
91  
92      /**
93       * Used to inject the signature helper that is used to to sign the ZIP archive.
94       *
95       * @param helper The helper.
96       */
97      public void setSignatureHelper(final SignatureHelper helper) {
98          signatureHelper = helper;
99      }
100 
101     /**
102      * Used to inject the archive helper that is used to output the CRX archive.
103      *
104      * @param helper The helper.
105      */
106     public void setArchiveHelper(final ArchiveHelper helper) {
107         archiveHelper = helper;
108     }
109 
110     /**
111      * Overriding the implementation in {@link org.codehaus.plexus.archiver.zip.AbstractZipArchiver} to set the
112      * packaging type to crx.
113      *
114      * @return Always returns {@code crx}.
115      */
116     @Override
117     protected String getArchiveType() {
118         return "crx";
119     }
120 
121     /**
122      * Generate an in-memory ZIP file containing the resources for the Google Chrome Extension, then sign the ZIP
123      * and write out a CRX file containing the header, signature, public key and ZIP data.
124      */
125     @Override
126     protected void execute() {
127 
128         try {
129             Security.addProvider(new BouncyCastleProvider());
130 
131             // ZIP the CRX source directory tree
132 
133             final byte[] zipData = createZipFile();
134 
135             // Get the public/private key and sign the ZIP
136 
137             final KeyPair keyPair = getKeyPair();
138             byte[] publicKey = keyPair.getPublic().getEncoded();
139             byte[] signature = signatureHelper.sign(zipData, keyPair.getPrivate());
140 
141             // Write the CRX file
142 
143             final CRXArchive archive = new CRXArchive(publicKey, signature, zipData);
144             archiveHelper.writeArchive(getDestFile(), archive);
145         } catch (final GeneralSecurityException e) {
146             throw new ArchiverException("Could not generate the signature for the CRX file", e);
147         } catch (final IOException e) {
148             throw new ArchiverException("Could not read resources or output the CRX file", e);
149         }
150     }
151 
152     /**
153      * Read the public/private key pair from a PEM file.
154      *
155      * @return The public/private key pair.
156      */
157     private KeyPair getKeyPair() {
158         try {
159             final Reader pemFileReader = new FileReader(pemFile);
160             try {
161                 final PEMParser pemParser = new PEMParser(pemFileReader);
162                 try {
163                     final Object pemObject = pemParser.readObject();
164                     if (pemObject instanceof KeyPair) {
165                         return (KeyPair) pemObject;
166                     } else if (pemObject instanceof PEMKeyPair) {
167                         final JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
168                         return converter.getKeyPair((PEMKeyPair) pemObject);
169                     } else if (pemObject instanceof PEMEncryptedKeyPair) {
170                         final JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
171                         final PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) pemObject;
172                         final PEMDecryptorProvider decryptorProvider = new BcPEMDecryptorProvider(pemPassword.toCharArray());
173                         final PEMKeyPair pemKeyPair = encryptedKeyPair.decryptKeyPair(decryptorProvider);
174                         return converter.getKeyPair(pemKeyPair);
175                     } else if (pemObject instanceof PrivateKeyInfo) {
176                         final JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
177                         final PrivateKey privateKey = converter.getPrivateKey((PrivateKeyInfo) pemObject);
178                         return convertRSAPrivateCrtKey(privateKey);
179                     } else {
180                         return convertRSAPrivateCrtKey(pemObject);
181                     }
182                 } finally {
183                     pemParser.close();
184                 }
185             } finally {
186                 pemFileReader.close();
187             }
188         } catch (final InvalidKeySpecException e) {
189             throw new ArchiverException("Cannot generate RSA public key", e);
190         } catch (final NoSuchAlgorithmException e) {
191             throw new ArchiverException("RSA Private key algorithm is not supported", e);
192         } catch (final NoSuchProviderException e) {
193             throw new ArchiverException("Bouncy Castle not registered correctly", e);
194         } catch (final IOException e) {
195             throw new ArchiverException("Could not load the public/private key from the PEM file", e);
196         }
197     }
198 
199     /**
200      * Attempt to convert a RSA private key to a public/private key pair.
201      *
202      * @param pemObject Object loaded from PEM file.
203      * @return The public/private key pair.
204      * @throws NoSuchAlgorithmException If the RSA algorithm is not supported.
205      * @throws NoSuchProviderException  If the Bouncy Castle provider is not registered.
206      * @throws InvalidKeySpecException  If the key specification is not supported.
207      */
208     private KeyPair convertRSAPrivateCrtKey(final Object pemObject)
209             throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
210         if (pemObject instanceof RSAPrivateCrtKey) {
211             final RSAPrivateCrtKey privateCrtKey = (RSAPrivateCrtKey) pemObject;
212             final BigInteger exponent = privateCrtKey.getPublicExponent();
213             final BigInteger modulus = privateCrtKey.getModulus();
214             final RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, exponent);
215             final KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
216             final PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
217             return new KeyPair(publicKey, privateCrtKey);
218         } else {
219             throw new ArchiverException("Could not load the public/private key from invalid PEM file");
220         }
221     }
222 
223     /**
224      * Create a ZIP file in memory containing the directory tree leveraging the {@link
225      * org.codehaus.plexus.archiver.zip.AbstractZipArchiver#addResources(org.codehaus.plexus.archiver.ResourceIterator,
226      * org.codehaus.plexus.archiver.zip.ZipOutputStream)} method to store resources in the ZIP file. The ZIP file is
227      * then converted to a byte array.
228      *
229      * @return A byte array containing the ZIP file.
230      * @throws java.io.IOException If there was an error reading the contents of the source directory.
231      */
232     private byte[] createZipFile() throws IOException {
233         final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
234         final ZipOutputStream out = new ZipOutputStream(buffer);
235         try {
236             out.setMethod(ZipOutputStream.DEFLATED);
237             out.setLevel(Deflater.BEST_COMPRESSION);
238             ResourceIterator resourceIterator = getResources();
239             addResources(resourceIterator, out);
240         } finally {
241             out.close();
242         }
243         return buffer.toByteArray();
244     }
245 }