1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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")
114 @Override
115 public int read() throws IOException {
116 return init().read();
117 }
118
119 @SuppressWarnings("resource")
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
129
130
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
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
217
218
219
220
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
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
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
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 }