1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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")
112 @Override
113 public int read() throws IOException {
114 return init().read();
115 }
116
117 @SuppressWarnings("resource")
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
127
128
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
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
215
216
217
218
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
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
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
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 }