Skip to content

Commit b891e82

Browse files
committed
Implement AES encryption
1 parent 7154fc7 commit b891e82

File tree

3 files changed

+138
-10
lines changed

3 files changed

+138
-10
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php namespace io\archive\zip;
2+
3+
use io\streams\InputStream;
4+
use lang\IllegalStateException;
5+
6+
/**
7+
* Deciphers using little-endian variant of AES-CTR
8+
*
9+
* @ext openssl
10+
*/
11+
class AESInputStream implements InputStream {
12+
const BLOCK= 16;
13+
14+
private $in, $key;
15+
private $cipher, $counter, $hmac;
16+
17+
/**
18+
* Constructor
19+
*
20+
* @param io.streams.InputStream $in
21+
* @param string $key
22+
* @param string $auth
23+
*/
24+
public function __construct($in, $key, $auth) {
25+
$this->in= $in;
26+
$this->key= $key;
27+
28+
$this->cipher= 'aes-'.(strlen($key) * 8).'-ecb';
29+
$this->counter= "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
30+
$this->hmac= hash_init('sha1', HASH_HMAC, $auth);
31+
}
32+
33+
/**
34+
* Decrypt, updating the HMAC and the counter while doing so
35+
*
36+
* @param string $input
37+
* @return string
38+
*/
39+
private function decrypt($input) {
40+
hash_update($this->hmac, $input);
41+
42+
$return= '';
43+
for ($offset= 0, $l= strlen($input); $offset < $l; $offset+= self::BLOCK) {
44+
45+
// Encrypt counter block using AES-ECB
46+
$keystream= openssl_encrypt(
47+
$this->counter,
48+
$this->cipher,
49+
$this->key,
50+
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
51+
);
52+
53+
// Take relevant part
54+
$return.= substr($input, $offset, self::BLOCK) ^ $keystream;
55+
56+
// Increment little-endian counter
57+
for ($j= 0, $carry= 1; $j < 16 && $carry; $j++) {
58+
$s= ord($this->counter[$j]) + $carry;
59+
$this->counter[$j]= chr($s & 0xff);
60+
$carry= $s >> 8;
61+
}
62+
}
63+
return $return;
64+
}
65+
66+
/**
67+
* Read a string
68+
*
69+
* @param int $limit default 8192
70+
* @return string
71+
* @throws lang.IllegalStateException when HMAC verification fails
72+
*/
73+
public function read($limit= 8192) {
74+
$chunk= $this->in->read($limit);
75+
if ($this->in->available()) return $this->decrypt($chunk);
76+
77+
// Verify HMAC checksum for last block
78+
$plain= $this->decrypt(substr($chunk, 0, -10));
79+
80+
$mac= hash_final($this->hmac, true);
81+
if (0 !== substr_compare($mac, substr($chunk, -10), 0, 10)) {
82+
throw new IllegalStateException('HMAC verification failed — corrupted data');
83+
}
84+
85+
return $plain;
86+
}
87+
88+
/**
89+
* Returns the number of bytes that can be read from this stream
90+
* without blocking.
91+
*
92+
* @return int
93+
*/
94+
public function available() {
95+
return $this->in->available();
96+
}
97+
98+
/**
99+
* Close this buffer
100+
*
101+
* @return void
102+
*/
103+
public function close() {
104+
$this->in->close();
105+
}
106+
}

src/main/php/io/archive/zip/AbstractZipReaderImpl.class.php

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use io\streams\InputStream;
44
use lang\{FormatException, IllegalArgumentException, MethodNotImplementedException};
5-
use util\Date;
5+
use util\{Date, Secret};
66

77
/**
88
* Abstract base class for zip reader implementations
@@ -34,14 +34,13 @@ public function __construct(InputStream $stream) {
3434
/**
3535
* Set password to use when extracting
3636
*
37-
* @param string password
37+
* @param ?string|util.Secret $password
3838
*/
3939
public function setPassword($password) {
4040
if (null === $password) {
4141
$this->password= null;
4242
} else {
43-
$this->password= new ZipCipher();
44-
$this->password->initialize(iconv(\xp::ENCODING, 'cp437', $password));
43+
$this->password= $password instanceof Secret ? $password : new Secret($password);
4544
}
4645
}
4746

@@ -229,12 +228,31 @@ public function currentEntry() {
229228
// AES vs. traditional PKZIP cipher
230229
if (99 === $header['compression']) {
231230
$aes= unpack('vheader/vsize/vversion/a2vendor/cstrength/vcompression', $extra);
231+
switch ($aes['strength']) {
232+
case 1: $sl= 8; $dl= 16; break;
233+
case 2: $sl= 12; $dl= 24; break;
234+
case 3: $sl= 16; $dl= 32; break;
235+
default: throw new IllegalArgumentException('Invalid AES strength '.$aes['strength']);
236+
}
232237

233-
// TODO: Implement
238+
// Verify password
239+
$salt= $this->streamRead($sl);
240+
$pvv= $this->streamRead(2);
241+
$dk= hash_pbkdf2('sha1', $this->password->reveal(), $salt, 1000, 2 * $dl + 2, true);
242+
if (0 !== substr_compare($dk, $pvv, 64, 2)) {
243+
throw new IllegalArgumentException('The password did not match');
244+
}
234245

235-
throw new MethodNotImplementedException('Not yet implemented', 'AES');
246+
$this->skip-= $sl + 2;
247+
$header['compression']= $aes['compression'];
248+
$is= new AESInputStream(
249+
new ZipFileInputStream($this, $this->position, $header['compressed'] - $sl - 2),
250+
substr($dk, 0, 32),
251+
substr($dk, 32, 32)
252+
);
236253
} else if ($header['flags'] & 1) {
237-
$cipher= new ZipCipher($this->password);
254+
$cipher= new ZipCipher();
255+
$cipher->initialize(iconv(\xp::ENCODING, 'cp437', $this->password->reveal()));
238256
$preamble= $cipher->decipher($this->streamRead(12));
239257

240258
// Verify
@@ -243,9 +261,12 @@ public function currentEntry() {
243261
}
244262

245263
// Password matches.
246-
$this->skip-= 12;
264+
$this->skip-= 12;
247265
$header['compressed']-= 12;
248-
$is= new DecipheringInputStream(new ZipFileInputStream($this, $this->position, $header['compressed']), $cipher);
266+
$is= new DecipheringInputStream(
267+
new ZipFileInputStream($this, $this->position, $header['compressed']),
268+
$cipher
269+
);
249270
} else {
250271
$is= new ZipFileInputStream($this, $this->position, $header['compressed']);
251272
}

src/main/php/io/archive/zip/ZipFileInputStream.class.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<?php namespace io\archive\zip;
22

33
use io\IOException;
4+
use io\streams\InputStream;
45

56
/**
67
* Zip File input stream. Reads from the current position up until a
78
* certain length.
89
*/
9-
class ZipFileInputStream implements \io\streams\InputStream {
10+
class ZipFileInputStream implements InputStream {
1011
protected
1112
$reader = null,
1213
$start = 0,

0 commit comments

Comments
 (0)