2015-04-09 15:59:03 +03:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* @author Björn Schießle <schiessle@owncloud.com>
|
2015-06-25 12:43:55 +03:00
|
|
|
* @author Joas Schilling <nickvergessen@owncloud.com>
|
2016-03-01 19:25:15 +03:00
|
|
|
* @author Lukas Reschke <lukas@owncloud.com>
|
2015-04-09 15:59:03 +03:00
|
|
|
*
|
2016-01-12 17:02:16 +03:00
|
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
2015-04-09 15:59:03 +03:00
|
|
|
* @license AGPL-3.0
|
|
|
|
*
|
|
|
|
* This code is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License, version 3,
|
|
|
|
* as published by the Free Software Foundation.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3,
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2015-04-27 16:23:14 +03:00
|
|
|
namespace OCA\Encryption\Tests\lib\Crypto;
|
2015-04-09 15:59:03 +03:00
|
|
|
|
|
|
|
|
|
|
|
use OCA\Encryption\Crypto\Crypt;
|
|
|
|
use Test\TestCase;
|
|
|
|
|
|
|
|
class cryptTest extends TestCase {
|
|
|
|
|
|
|
|
|
|
|
|
/** @var \PHPUnit_Framework_MockObject_MockObject */
|
|
|
|
private $logger;
|
|
|
|
|
|
|
|
/** @var \PHPUnit_Framework_MockObject_MockObject */
|
|
|
|
private $userSession;
|
|
|
|
|
|
|
|
/** @var \PHPUnit_Framework_MockObject_MockObject */
|
|
|
|
private $config;
|
|
|
|
|
2016-01-05 14:51:05 +03:00
|
|
|
|
|
|
|
/** @var \PHPUnit_Framework_MockObject_MockObject */
|
|
|
|
private $l;
|
|
|
|
|
2015-04-09 15:59:03 +03:00
|
|
|
/** @var Crypt */
|
|
|
|
private $crypt;
|
|
|
|
|
|
|
|
public function setUp() {
|
|
|
|
parent::setUp();
|
|
|
|
|
|
|
|
$this->logger = $this->getMockBuilder('OCP\ILogger')
|
|
|
|
->disableOriginalConstructor()
|
|
|
|
->getMock();
|
|
|
|
$this->logger->expects($this->any())
|
|
|
|
->method('warning')
|
|
|
|
->willReturn(true);
|
|
|
|
$this->userSession = $this->getMockBuilder('OCP\IUserSession')
|
|
|
|
->disableOriginalConstructor()
|
|
|
|
->getMock();
|
|
|
|
$this->config = $this->getMockBuilder('OCP\IConfig')
|
|
|
|
->disableOriginalConstructor()
|
|
|
|
->getMock();
|
2016-01-05 14:51:05 +03:00
|
|
|
$this->l = $this->getMock('OCP\IL10N');
|
2015-04-09 15:59:03 +03:00
|
|
|
|
2016-01-05 14:51:05 +03:00
|
|
|
$this->crypt = new Crypt($this->logger, $this->userSession, $this->config, $this->l);
|
2015-04-09 15:59:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test getOpenSSLConfig without any additional parameters
|
|
|
|
*/
|
|
|
|
public function testGetOpenSSLConfigBasic() {
|
|
|
|
|
|
|
|
$this->config->expects($this->once())
|
|
|
|
->method('getSystemValue')
|
|
|
|
->with($this->equalTo('openssl'), $this->equalTo([]))
|
|
|
|
->willReturn(array());
|
|
|
|
|
2015-06-03 13:03:02 +03:00
|
|
|
$result = self::invokePrivate($this->crypt, 'getOpenSSLConfig');
|
2015-04-09 15:59:03 +03:00
|
|
|
$this->assertSame(1, count($result));
|
|
|
|
$this->assertArrayHasKey('private_key_bits', $result);
|
|
|
|
$this->assertSame(4096, $result['private_key_bits']);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test getOpenSSLConfig with additional parameters defined in config.php
|
|
|
|
*/
|
|
|
|
public function testGetOpenSSLConfig() {
|
|
|
|
|
|
|
|
$this->config->expects($this->once())
|
|
|
|
->method('getSystemValue')
|
|
|
|
->with($this->equalTo('openssl'), $this->equalTo([]))
|
|
|
|
->willReturn(array('foo' => 'bar', 'private_key_bits' => 1028));
|
|
|
|
|
2015-06-03 13:03:02 +03:00
|
|
|
$result = self::invokePrivate($this->crypt, 'getOpenSSLConfig');
|
2015-04-09 15:59:03 +03:00
|
|
|
$this->assertSame(2, count($result));
|
|
|
|
$this->assertArrayHasKey('private_key_bits', $result);
|
|
|
|
$this->assertArrayHasKey('foo', $result);
|
|
|
|
$this->assertSame(1028, $result['private_key_bits']);
|
|
|
|
$this->assertSame('bar', $result['foo']);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2015-08-07 15:04:17 +03:00
|
|
|
* test generateHeader with valid key formats
|
|
|
|
*
|
|
|
|
* @dataProvider dataTestGenerateHeader
|
2015-04-09 15:59:03 +03:00
|
|
|
*/
|
2015-08-07 15:04:17 +03:00
|
|
|
public function testGenerateHeader($keyFormat, $expected) {
|
2015-04-09 15:59:03 +03:00
|
|
|
|
|
|
|
$this->config->expects($this->once())
|
|
|
|
->method('getSystemValue')
|
2016-01-04 23:00:55 +03:00
|
|
|
->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR'))
|
2015-04-09 15:59:03 +03:00
|
|
|
->willReturn('AES-128-CFB');
|
|
|
|
|
2015-08-07 15:04:17 +03:00
|
|
|
if ($keyFormat) {
|
|
|
|
$result = $this->crypt->generateHeader($keyFormat);
|
|
|
|
} else {
|
|
|
|
$result = $this->crypt->generateHeader();
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->assertSame($expected, $result);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test generateHeader with invalid key format
|
|
|
|
*
|
|
|
|
* @expectedException \InvalidArgumentException
|
|
|
|
*/
|
|
|
|
public function testGenerateHeaderInvalid() {
|
|
|
|
$this->crypt->generateHeader('unknown');
|
|
|
|
}
|
|
|
|
|
2016-01-04 23:00:55 +03:00
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
2015-08-07 15:04:17 +03:00
|
|
|
public function dataTestGenerateHeader() {
|
|
|
|
return [
|
|
|
|
[null, 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:HEND'],
|
|
|
|
['password', 'HBEGIN:cipher:AES-128-CFB:keyFormat:password:HEND'],
|
|
|
|
['hash', 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:HEND']
|
|
|
|
];
|
2015-04-09 15:59:03 +03:00
|
|
|
}
|
|
|
|
|
2016-01-04 23:00:55 +03:00
|
|
|
public function testGetCipherWithInvalidCipher() {
|
|
|
|
$this->config->expects($this->once())
|
|
|
|
->method('getSystemValue')
|
|
|
|
->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR'))
|
|
|
|
->willReturn('Not-Existing-Cipher');
|
|
|
|
$this->logger
|
|
|
|
->expects($this->once())
|
|
|
|
->method('warning')
|
|
|
|
->with('Unsupported cipher (Not-Existing-Cipher) defined in config.php supported. Falling back to AES-256-CTR');
|
|
|
|
|
|
|
|
$this->assertSame('AES-256-CTR', $this->crypt->getCipher());
|
|
|
|
}
|
|
|
|
|
2015-04-09 15:59:03 +03:00
|
|
|
/**
|
|
|
|
* @dataProvider dataProviderGetCipher
|
|
|
|
* @param string $configValue
|
|
|
|
* @param string $expected
|
|
|
|
*/
|
|
|
|
public function testGetCipher($configValue, $expected) {
|
|
|
|
$this->config->expects($this->once())
|
|
|
|
->method('getSystemValue')
|
2016-01-04 23:00:55 +03:00
|
|
|
->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR'))
|
2015-04-09 15:59:03 +03:00
|
|
|
->willReturn($configValue);
|
|
|
|
|
|
|
|
$this->assertSame($expected,
|
|
|
|
$this->crypt->getCipher()
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* data provider for testGetCipher
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function dataProviderGetCipher() {
|
|
|
|
return array(
|
|
|
|
array('AES-128-CFB', 'AES-128-CFB'),
|
|
|
|
array('AES-256-CFB', 'AES-256-CFB'),
|
2016-01-04 23:00:55 +03:00
|
|
|
array('AES-128-CTR', 'AES-128-CTR'),
|
|
|
|
array('AES-256-CTR', 'AES-256-CTR'),
|
|
|
|
|
|
|
|
array('unknown', 'AES-256-CTR')
|
2015-04-09 15:59:03 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test concatIV()
|
|
|
|
*/
|
|
|
|
public function testConcatIV() {
|
|
|
|
|
2015-06-03 13:03:02 +03:00
|
|
|
$result = self::invokePrivate(
|
2015-04-09 15:59:03 +03:00
|
|
|
$this->crypt,
|
|
|
|
'concatIV',
|
|
|
|
array('content', 'my_iv'));
|
|
|
|
|
|
|
|
$this->assertSame('content00iv00my_iv',
|
|
|
|
$result
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-01-05 21:01:03 +03:00
|
|
|
* @dataProvider dataTestSplitMetaData
|
2015-04-09 15:59:03 +03:00
|
|
|
*/
|
2016-01-05 21:01:03 +03:00
|
|
|
public function testSplitMetaData($data, $expected) {
|
|
|
|
$result = self::invokePrivate($this->crypt, 'splitMetaData', array($data, 'AES-256-CFB'));
|
2015-04-09 15:59:03 +03:00
|
|
|
$this->assertTrue(is_array($result));
|
2016-01-05 21:01:03 +03:00
|
|
|
$this->assertSame(3, count($result));
|
2015-04-09 15:59:03 +03:00
|
|
|
$this->assertArrayHasKey('encrypted', $result);
|
|
|
|
$this->assertArrayHasKey('iv', $result);
|
2016-01-05 21:01:03 +03:00
|
|
|
$this->assertArrayHasKey('signature', $result);
|
|
|
|
$this->assertSame($expected['encrypted'], $result['encrypted']);
|
|
|
|
$this->assertSame($expected['iv'], $result['iv']);
|
|
|
|
$this->assertSame($expected['signature'], $result['signature']);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function dataTestSplitMetaData() {
|
|
|
|
return [
|
|
|
|
['encryptedContent00iv001234567890123456xx',
|
|
|
|
['encrypted' => 'encryptedContent', 'iv' => '1234567890123456', 'signature' => false]],
|
|
|
|
['encryptedContent00iv00123456789012345600sig00e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86xxx',
|
|
|
|
['encrypted' => 'encryptedContent', 'iv' => '1234567890123456', 'signature' => 'e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86']],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @dataProvider dataTestHasSignature
|
|
|
|
*/
|
|
|
|
public function testHasSignature($data, $expected) {
|
|
|
|
$this->assertSame($expected,
|
|
|
|
$this->invokePrivate($this->crypt, 'hasSignature', array($data, 'AES-256-CFB'))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function dataTestHasSignature() {
|
|
|
|
return [
|
|
|
|
['encryptedContent00iv001234567890123456xx', false],
|
|
|
|
['encryptedContent00iv00123456789012345600sig00e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86xxx', true]
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @dataProvider dataTestHasSignatureFail
|
|
|
|
* @expectedException \OC\HintException
|
|
|
|
*/
|
|
|
|
public function testHasSignatureFail($cipher) {
|
|
|
|
$data = 'encryptedContent00iv001234567890123456xx';
|
|
|
|
$this->invokePrivate($this->crypt, 'hasSignature', array($data, $cipher));
|
|
|
|
}
|
|
|
|
|
|
|
|
public function dataTestHasSignatureFail() {
|
|
|
|
return [
|
|
|
|
['AES-256-CTR'],
|
|
|
|
['aes-256-ctr'],
|
|
|
|
['AES-128-CTR'],
|
|
|
|
['ctr-256-ctr']
|
|
|
|
];
|
2015-04-09 15:59:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test addPadding()
|
|
|
|
*/
|
|
|
|
public function testAddPadding() {
|
2015-06-03 13:03:02 +03:00
|
|
|
$result = self::invokePrivate($this->crypt, 'addPadding', array('data'));
|
2016-01-05 21:01:03 +03:00
|
|
|
$this->assertSame('dataxxx', $result);
|
2015-04-09 15:59:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test removePadding()
|
|
|
|
*
|
|
|
|
* @dataProvider dataProviderRemovePadding
|
|
|
|
* @param $data
|
|
|
|
* @param $expected
|
|
|
|
*/
|
|
|
|
public function testRemovePadding($data, $expected) {
|
2015-06-03 13:03:02 +03:00
|
|
|
$result = self::invokePrivate($this->crypt, 'removePadding', array($data));
|
2015-04-09 15:59:03 +03:00
|
|
|
$this->assertSame($expected, $result);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* data provider for testRemovePadding
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function dataProviderRemovePadding() {
|
|
|
|
return array(
|
|
|
|
array('dataxx', 'data'),
|
|
|
|
array('data', false)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test parseHeader()
|
|
|
|
*/
|
|
|
|
public function testParseHeader() {
|
|
|
|
|
|
|
|
$header= 'HBEGIN:foo:bar:cipher:AES-256-CFB:HEND';
|
2015-06-03 13:03:02 +03:00
|
|
|
$result = self::invokePrivate($this->crypt, 'parseHeader', array($header));
|
2015-04-09 15:59:03 +03:00
|
|
|
|
|
|
|
$this->assertTrue(is_array($result));
|
|
|
|
$this->assertSame(2, count($result));
|
|
|
|
$this->assertArrayHasKey('foo', $result);
|
|
|
|
$this->assertArrayHasKey('cipher', $result);
|
|
|
|
$this->assertSame('bar', $result['foo']);
|
|
|
|
$this->assertSame('AES-256-CFB', $result['cipher']);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test encrypt()
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function testEncrypt() {
|
|
|
|
|
|
|
|
$decrypted = 'content';
|
|
|
|
$password = 'password';
|
2015-06-03 13:03:02 +03:00
|
|
|
$iv = self::invokePrivate($this->crypt, 'generateIv');
|
2015-04-09 15:59:03 +03:00
|
|
|
|
|
|
|
$this->assertTrue(is_string($iv));
|
|
|
|
$this->assertSame(16, strlen($iv));
|
|
|
|
|
2015-06-03 13:03:02 +03:00
|
|
|
$result = self::invokePrivate($this->crypt, 'encrypt', array($decrypted, $iv, $password));
|
2015-04-09 15:59:03 +03:00
|
|
|
|
|
|
|
$this->assertTrue(is_string($result));
|
|
|
|
|
|
|
|
return array(
|
|
|
|
'password' => $password,
|
|
|
|
'iv' => $iv,
|
|
|
|
'encrypted' => $result,
|
|
|
|
'decrypted' => $decrypted);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test decrypt()
|
|
|
|
*
|
|
|
|
* @depends testEncrypt
|
|
|
|
*/
|
|
|
|
public function testDecrypt($data) {
|
|
|
|
|
2015-06-03 13:03:02 +03:00
|
|
|
$result = self::invokePrivate(
|
2015-04-09 15:59:03 +03:00
|
|
|
$this->crypt,
|
|
|
|
'decrypt',
|
|
|
|
array($data['encrypted'], $data['iv'], $data['password']));
|
|
|
|
|
|
|
|
$this->assertSame($data['decrypted'], $result);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2015-08-07 15:04:17 +03:00
|
|
|
/**
|
|
|
|
* test return values of valid ciphers
|
|
|
|
*
|
|
|
|
* @dataProvider dataTestGetKeySize
|
|
|
|
*/
|
|
|
|
public function testGetKeySize($cipher, $expected) {
|
|
|
|
$result = $this->invokePrivate($this->crypt, 'getKeySize', [$cipher]);
|
|
|
|
$this->assertSame($expected, $result);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* test exception if cipher is unknown
|
|
|
|
*
|
|
|
|
* @expectedException \InvalidArgumentException
|
|
|
|
*/
|
|
|
|
public function testGetKeySizeFailure() {
|
|
|
|
$this->invokePrivate($this->crypt, 'getKeySize', ['foo']);
|
|
|
|
}
|
|
|
|
|
2016-01-04 23:00:55 +03:00
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
2015-08-07 15:04:17 +03:00
|
|
|
public function dataTestGetKeySize() {
|
|
|
|
return [
|
|
|
|
['AES-256-CFB', 32],
|
|
|
|
['AES-128-CFB', 16],
|
2016-01-04 23:00:55 +03:00
|
|
|
['AES-256-CTR', 32],
|
|
|
|
['AES-128-CTR', 16],
|
2015-08-07 15:04:17 +03:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @dataProvider dataTestDecryptPrivateKey
|
|
|
|
*/
|
|
|
|
public function testDecryptPrivateKey($header, $privateKey, $expectedCipher, $isValidKey, $expected) {
|
|
|
|
/** @var \OCA\Encryption\Crypto\Crypt | \PHPUnit_Framework_MockObject_MockObject $crypt */
|
|
|
|
$crypt = $this->getMockBuilder('OCA\Encryption\Crypto\Crypt')
|
|
|
|
->setConstructorArgs(
|
|
|
|
[
|
|
|
|
$this->logger,
|
|
|
|
$this->userSession,
|
2016-01-05 21:01:03 +03:00
|
|
|
$this->config,
|
|
|
|
$this->l
|
2015-08-07 15:04:17 +03:00
|
|
|
]
|
|
|
|
)
|
|
|
|
->setMethods(
|
|
|
|
[
|
|
|
|
'parseHeader',
|
|
|
|
'generatePasswordHash',
|
|
|
|
'symmetricDecryptFileContent',
|
|
|
|
'isValidPrivateKey'
|
|
|
|
]
|
|
|
|
)
|
|
|
|
->getMock();
|
|
|
|
|
|
|
|
$crypt->expects($this->once())->method('parseHeader')->willReturn($header);
|
|
|
|
if (isset($header['keyFormat']) && $header['keyFormat'] === 'hash') {
|
|
|
|
$crypt->expects($this->once())->method('generatePasswordHash')->willReturn('hash');
|
|
|
|
$password = 'hash';
|
|
|
|
} else {
|
|
|
|
$crypt->expects($this->never())->method('generatePasswordHash');
|
|
|
|
$password = 'password';
|
|
|
|
}
|
|
|
|
|
|
|
|
$crypt->expects($this->once())->method('symmetricDecryptFileContent')
|
|
|
|
->with('privateKey', $password, $expectedCipher)->willReturn('key');
|
|
|
|
$crypt->expects($this->once())->method('isValidPrivateKey')->willReturn($isValidKey);
|
|
|
|
|
|
|
|
$result = $crypt->decryptPrivateKey($privateKey, 'password');
|
|
|
|
|
|
|
|
$this->assertSame($expected, $result);
|
|
|
|
}
|
|
|
|
|
2016-01-04 23:00:55 +03:00
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
2015-08-07 15:04:17 +03:00
|
|
|
public function dataTestDecryptPrivateKey() {
|
|
|
|
return [
|
|
|
|
[['cipher' => 'AES-128-CFB', 'keyFormat' => 'password'], 'HBEGIN:HENDprivateKey', 'AES-128-CFB', true, 'key'],
|
|
|
|
[['cipher' => 'AES-256-CFB', 'keyFormat' => 'password'], 'HBEGIN:HENDprivateKey', 'AES-256-CFB', true, 'key'],
|
|
|
|
[['cipher' => 'AES-256-CFB', 'keyFormat' => 'password'], 'HBEGIN:HENDprivateKey', 'AES-256-CFB', false, false],
|
|
|
|
[['cipher' => 'AES-256-CFB', 'keyFormat' => 'hash'], 'HBEGIN:HENDprivateKey', 'AES-256-CFB', true, 'key'],
|
|
|
|
[['cipher' => 'AES-256-CFB'], 'HBEGIN:HENDprivateKey', 'AES-256-CFB', true, 'key'],
|
|
|
|
[[], 'privateKey', 'AES-128-CFB', true, 'key'],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2015-08-25 16:58:07 +03:00
|
|
|
public function testIsValidPrivateKey() {
|
|
|
|
$res = openssl_pkey_new();
|
|
|
|
openssl_pkey_export($res, $privateKey);
|
|
|
|
|
|
|
|
// valid private key
|
|
|
|
$this->assertTrue(
|
|
|
|
$this->invokePrivate($this->crypt, 'isValidPrivateKey', [$privateKey])
|
|
|
|
);
|
|
|
|
|
|
|
|
// invalid private key
|
|
|
|
$this->assertFalse(
|
|
|
|
$this->invokePrivate($this->crypt, 'isValidPrivateKey', ['foo'])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2015-04-09 15:59:03 +03:00
|
|
|
}
|