View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package org.apache.commons.compress.archivers.sevenz;
18  
19  import static java.nio.charset.StandardCharsets.UTF_16LE;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.nio.ByteBuffer;
25  import java.nio.CharBuffer;
26  import java.security.GeneralSecurityException;
27  import java.security.MessageDigest;
28  import java.security.NoSuchAlgorithmException;
29  import java.util.Arrays;
30  
31  import javax.crypto.Cipher;
32  import javax.crypto.CipherInputStream;
33  import javax.crypto.CipherOutputStream;
34  import javax.crypto.SecretKey;
35  import javax.crypto.spec.IvParameterSpec;
36  
37  import org.apache.commons.compress.PasswordRequiredException;
38  
39  final class AES256SHA256Decoder extends AbstractCoder {
40  
41      private static final class AES256SHA256DecoderInputStream extends InputStream {
42          private final InputStream in;
43          private final Coder coder;
44          private final String archiveName;
45          private final byte[] passwordBytes;
46          private boolean isInitialized;
47          private CipherInputStream cipherInputStream;
48  
49          private AES256SHA256DecoderInputStream(final InputStream in, final Coder coder, final String archiveName, final byte[] passwordBytes) {
50              this.in = in;
51              this.coder = coder;
52              this.archiveName = archiveName;
53              this.passwordBytes = passwordBytes;
54          }
55  
56          @Override
57          public void close() throws IOException {
58              if (cipherInputStream != null) {
59                  cipherInputStream.close();
60              }
61          }
62  
63          private CipherInputStream init() throws IOException {
64              if (isInitialized) {
65                  return cipherInputStream;
66              }
67              if (coder.properties == null) {
68                  throw new IOException("Missing AES256 properties in " + archiveName);
69              }
70              if (coder.properties.length < 2) {
71                  throw new IOException("AES256 properties too short in " + archiveName);
72              }
73              final int byte0 = 0xff & coder.properties[0];
74              final int numCyclesPower = byte0 & 0x3f;
75              final int byte1 = 0xff & coder.properties[1];
76              final int ivSize = (byte0 >> 6 & 1) + (byte1 & 0x0f);
77              final int saltSize = (byte0 >> 7 & 1) + (byte1 >> 4);
78              if (2 + saltSize + ivSize > coder.properties.length) {
79                  throw new IOException("Salt size + IV size too long in " + archiveName);
80              }
81              final byte[] salt = new byte[saltSize];
82              System.arraycopy(coder.properties, 2, salt, 0, saltSize);
83              final byte[] iv = new byte[16];
84              System.arraycopy(coder.properties, 2 + saltSize, iv, 0, ivSize);
85  
86              if (passwordBytes == null) {
87                  throw new PasswordRequiredException(archiveName);
88              }
89              final byte[] aesKeyBytes;
90              if (numCyclesPower == 0x3f) {
91                  aesKeyBytes = new byte[32];
92                  System.arraycopy(salt, 0, aesKeyBytes, 0, saltSize);
93                  System.arraycopy(passwordBytes, 0, aesKeyBytes, saltSize, Math.min(passwordBytes.length, aesKeyBytes.length - saltSize));
94              } else {
95                  aesKeyBytes = sha256Password(passwordBytes, numCyclesPower, salt);
96              }
97  
98              final SecretKey aesKey = AES256Options.newSecretKeySpec(aesKeyBytes);
99              try {
100                 final Cipher cipher = Cipher.getInstance(AES256Options.TRANSFORMATION);
101                 cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(iv));
102                 cipherInputStream = new CipherInputStream(in, cipher);
103                 isInitialized = true;
104                 return cipherInputStream;
105             } catch (final GeneralSecurityException generalSecurityException) {
106                 throw new IllegalStateException("Decryption error (do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)",
107                         generalSecurityException);
108             }
109         }
110 
111         @SuppressWarnings("resource") // Closed in close()
112         @Override
113         public int read() throws IOException {
114             return init().read();
115         }
116 
117         @SuppressWarnings("resource") // Closed in close()
118         @Override
119         public int read(final byte[] b, final int off, final int len) throws IOException {
120             return init().read(b, off, len);
121         }
122     }
123 
124     private static final class AES256SHA256DecoderOutputStream extends OutputStream {
125         private final CipherOutputStream cipherOutputStream;
126         // Ensures that data are encrypt in respect of cipher block size and pad with '0' if smaller
127         // NOTE: As "AES/CBC/PKCS5Padding" is weak and should not be used, we use "AES/CBC/NoPadding" with this
128         // manual implementation for padding possible thanks to the size of the file stored separately
129         private final int cipherBlockSize;
130         private final byte[] cipherBlockBuffer;
131         private int count;
132 
133         private AES256SHA256DecoderOutputStream(final AES256Options opts, final OutputStream out) {
134             cipherOutputStream = new CipherOutputStream(out, opts.getCipher());
135             cipherBlockSize = opts.getCipher().getBlockSize();
136             cipherBlockBuffer = new byte[cipherBlockSize];
137         }
138 
139         @Override
140         public void close() throws IOException {
141             if (count > 0) {
142                 cipherOutputStream.write(cipherBlockBuffer);
143             }
144             cipherOutputStream.close();
145         }
146 
147         @Override
148         public void flush() throws IOException {
149             cipherOutputStream.flush();
150         }
151 
152         private void flushBuffer() throws IOException {
153             cipherOutputStream.write(cipherBlockBuffer);
154             count = 0;
155             Arrays.fill(cipherBlockBuffer, (byte) 0);
156         }
157 
158         @Override
159         public void write(final byte[] b, final int off, final int len) throws IOException {
160             int gap = len + count > cipherBlockSize ? cipherBlockSize - count : len;
161             System.arraycopy(b, off, cipherBlockBuffer, count, gap);
162             count += gap;
163 
164             if (count == cipherBlockSize) {
165                 flushBuffer();
166 
167                 if (len - gap >= cipherBlockSize) {
168                     // skip buffer to encrypt data chunks big enough to fit cipher block size
169                     final int multipleCipherBlockSizeLen = (len - gap) / cipherBlockSize * cipherBlockSize;
170                     cipherOutputStream.write(b, off + gap, multipleCipherBlockSizeLen);
171                     gap += multipleCipherBlockSizeLen;
172                 }
173                 System.arraycopy(b, off + gap, cipherBlockBuffer, 0, len - gap);
174                 count = len - gap;
175             }
176         }
177 
178         @Override
179         public void write(final int b) throws IOException {
180             cipherBlockBuffer[count++] = (byte) b;
181             if (count == cipherBlockSize) {
182                 flushBuffer();
183             }
184         }
185     }
186 
187     static byte[] sha256Password(final byte[] password, final int numCyclesPower, final byte[] salt) {
188         final MessageDigest digest;
189         try {
190             digest = MessageDigest.getInstance("SHA-256");
191         } catch (final NoSuchAlgorithmException noSuchAlgorithmException) {
192             throw new IllegalStateException("SHA-256 is unsupported by your Java implementation", noSuchAlgorithmException);
193         }
194         final byte[] extra = new byte[8];
195         for (long j = 0; j < 1L << numCyclesPower; j++) {
196             digest.update(salt);
197             digest.update(password);
198             digest.update(extra);
199             for (int k = 0; k < extra.length; k++) {
200                 ++extra[k];
201                 if (extra[k] != 0) {
202                     break;
203                 }
204             }
205         }
206         return digest.digest();
207     }
208 
209     static byte[] sha256Password(final char[] password, final int numCyclesPower, final byte[] salt) {
210         return sha256Password(utf16Decode(password), numCyclesPower, salt);
211     }
212 
213     /**
214      * Convenience method that encodes Unicode characters into bytes in UTF-16 (little-endian byte order) charset
215      *
216      * @param chars characters to encode
217      * @return encoded characters
218      * @since 1.23
219      */
220     static byte[] utf16Decode(final char[] chars) {
221         if (chars == null) {
222             return null;
223         }
224         final ByteBuffer encoded = UTF_16LE.encode(CharBuffer.wrap(chars));
225         if (encoded.hasArray()) {
226             return encoded.array();
227         }
228         final byte[] e = new byte[encoded.remaining()];
229         encoded.get(e);
230         return e;
231     }
232 
233     AES256SHA256Decoder() {
234         super(AES256Options.class);
235     }
236 
237     @Override
238     InputStream decode(final String archiveName, final InputStream in, final long uncompressedLength, final Coder coder, final byte[] passwordBytes,
239             final int maxMemoryLimitInKb) {
240         return new AES256SHA256DecoderInputStream(in, coder, archiveName, passwordBytes);
241     }
242 
243     @Override
244     OutputStream encode(final OutputStream out, final Object options) throws IOException {
245         return new AES256SHA256DecoderOutputStream((AES256Options) options, out);
246     }
247 
248     @Override
249     byte[] getOptionsAsProperties(final Object options) throws IOException {
250         final AES256Options opts = (AES256Options) options;
251         final byte[] props = new byte[2 + opts.getSalt().length + opts.getIv().length];
252 
253         // First byte : control (numCyclesPower + flags of salt or iv presence)
254         props[0] = (byte) (opts.getNumCyclesPower() | (opts.getSalt().length == 0 ? 0 : 1 << 7) | (opts.getIv().length == 0 ? 0 : 1 << 6));
255 
256         if (opts.getSalt().length != 0 || opts.getIv().length != 0) {
257             // second byte : size of salt/iv data
258             props[1] = (byte) ((opts.getSalt().length == 0 ? 0 : opts.getSalt().length - 1) << 4 | (opts.getIv().length == 0 ? 0 : opts.getIv().length - 1));
259 
260             // remain bytes : salt/iv data
261             System.arraycopy(opts.getSalt(), 0, props, 2, opts.getSalt().length);
262             System.arraycopy(opts.getIv(), 0, props, 2 + opts.getSalt().length, opts.getIv().length);
263         }
264 
265         return props;
266     }
267 }