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