2023 년경 현대 PHP 암호화 도서관.
개요:
이 라이브러리는 PHP 나트륨 라이브러리와 PHP OpenSSL 라이브러리 주변의 포장지입니다.
이 라이브러리의 나트륨 코드는 PHP 나트륨 _crypto_secretbox () 함수에 대한 문서에 주어진 예제를 기반으로합니다.
이 라이브러리의 OpenSSL 코드는 PHP OpenSSL_encrypt () 함수에 대한 문서에 주어진 예제를 기반으로합니다.
이 라이브러리는 암호화하는 데이터가 암호를 암호화하는 데 사용되는 비밀 키만큼 안전한 지 확인하는 것을 목표로합니다. 또한 암호화 된 데이터가 변조 방지되도록 조치를 취합니다.
이 라이브러리는 키 관리의 어려운 문제를 해결할 수 없습니다.

이 라이브러리는 진행중인 작업입니다.
이 코드를 친구 및 동료와 공유하고 가능한 한 많은 비판과 피드백을 요청합니다. 이 라이브러리가 내가 만들 수있는만큼 좋다고 생각하면이 상태 노트를 업데이트 할 것입니다. 그 동안 변화를 끊는 것은 거의 확실하며 암호화 약점은 상당히 가능합니다. 내가 알아야 할 것을 발견하면 알려주세요!
나는이 도서관이 크고 복잡하고 많은 사용을 보지 못했다는 것을 분명히하고 싶습니다. 의심 할 여지없이 아직 발견되지 않은 미묘한 버그가 있습니다. 이 코드 기반은 견고하고 신뢰할 수있는 도구로 성숙 할 가능성이 있지만 프로세스를 진행해야합니다.
이 섹션을 읽으십시오.
암호화 코드로 잘못 갈 수있는 방법에는 여러 가지가 있습니다. 이 도서관은 암호화 풋군을 줄이기위한 시도로 작성되었습니다. 바라건대 그것은 아무것도 소개하지 않았습니다!
암호화에 대해 가장 먼저 알아야 할 것은 데이터가 키만큼 안전하다는 것입니다. 여기서 말할 수있는 것보다 키 관리에 대해 알아야 할 것이 더 많지만 (어쨌든 나는 전문가가 아닙니다) 여기에 몇 가지 생각이 있습니다.
알아야 할 다른 것들 :
get_error() 호출해야합니다.내가 알았을 때 나를 놀라게했던 또 다른 것은, 당신이 알면 꽤 분명하지만, 데이터를 암호화하기 전에 데이터를 압축 해서는 안된다는 것입니다. 이것은 항상 문제가되는 것은 아니지만 특정 상황에서는 그럴 수 있으므로 결코 그렇게하는 것이 가장 좋습니다.
압축 문제는 공격자가 일부 입력 데이터를 제어 할 수 있다면 특정 값을 포함 할 수 있고 출력의 크기가 감소하면 다른 입력이 특정 값에 포함되어 있음을 알 수 있다는 것입니다. 아야.
이 코드베이스는 성숙하거나 잘 테스트되지 않았으며, 사용하기 전에 모든 코드를 읽어 품질 표준을 충족해야합니다. 당신이 그렇게한다면 나는 당신의 의견을 기뻐할 것입니다.
모든 사람이 알아야 할 다른 것을 생각할 수 있다면주의하십시오.
rtfm ..? 그리고 여기, 나는이 모든 것들을 쓰고있다 ... 적어도 위에 나열된 경고를 읽으십시오.
#!/bin/bash
set -euo pipefail;
mkdir -p kickass-demo/lib
cd kickass-demo
git clone https://github.com/jj5/kickass-crypto.git lib/kickass-crypto 2>/dev/null
php lib/kickass-crypto/bin/gen-demo-config.php > config.php
cat > demo.php <<'EOF'
<?php
require_once __DIR__ . '/lib/kickass-crypto/inc/sodium.php';
require_once __DIR__ . '/config.php';
$ciphertext = kickass_round_trip()->encrypt( 'secret text' );
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
echo "the secret data is: $plaintext.n";
EOF
php demo.php
약간 더 자세히 설명하려면 샘플 코드를 확인하십시오.
또는이 라이브러리 작동 방식에 대한 결론을 원한다면 라이브러리 프레임 워크 또는 다른 코드에서 코드를 읽으십시오.
Gee, 그것은 단순히 충분히 시작했지만 결국 복잡해졌습니다.
나는 서버와 클라이언트 간의 비교적 민감한 데이터 (낙관적 동시성 제어를위한 행 버전 번호)를 비교적 안전한 방식으로 비교적 안전한 방식, 비밀 및 훼손된 방지 선호하는 것을 왕복하고 싶었습니다.
OpenSSL 라이브러리가 PHP에서 사용할 수 있다고 들었으므로 사용 방법에 관한 정보를 검색했습니다. OpenSSL_encrypt () 함수에 대한 PHP 문서에서 예제 코드를 찾았습니다.
처음에는이 코드를 사용하는 방법이 명확하지 않았습니다. 특히 인증 태그, 초기화 벡터 및 암호 텍스트의 세 부분으로 무엇을 해야하는지 알아 내기가 어려웠습니다. 결국 나는 단지 그들을 동의 할 수 있다는 것을 알았습니다. 그러나 내가 그렇게한다면 나중에 검색 할 수 있도록 길이와 배치를 표준화해야합니다 ...
... 그런 다음 특정 경계에서 고정 길이로 패딩하여 실제 데이터 크기를 마스킹하는 것이 더 낫다고 생각했습니다.
... 그리고 나는 어떤 형태의 직렬화를 요구하는 풍부한 데이터를 지원하고 싶었습니다. 처음에는 php serialize () 함수를 사용하고 있었지만 나중에 json_encode ()로 변경되었습니다.
예제 코드는 지원되는 방식으로 키를 회전시키는 방법에 대해 전혀 나타내지 않았습니다. 그래서 나는이 라이브러리에서 지원하는 두 가지 사용 사례를 생각해 냈습니다. 이 라이브러리를 사용하면 새 키로 회전 할 수 있습니다.
그런 다음 예외 처리 및 오류보고, 일부 단위 테스트 및 검증, 타이밍 공격 완화, 서비스 로케이터, 사용법 데모, 데이터 크기 제한, 패스 프레이즈 초기화, 주요 생성 스크립트, 원격 측정 및 이와 유사한 것들에 대한 신중한 접근 방식을 계층화했습니다.
기본적 으로이 전체 라이브러리는 내가 내장 된 PHP OpenSSL 라이브러리 구현을 실제로 사용할 수 있도록해야한다고 생각했던 모든 것입니다.
그리고 ... 사람들은 나트륨 라이브러리에 대해 말해주기 시작했고, 대신 그것을 사용한다고 제안했습니다. 키 관리 및 입력 직렬화 및 메시지 서식 및 인코딩을 위해 이미 많은 작업을 수행했기 때문에 나는 그 모든 것을 재사용하고 나트륨 주위에 래퍼를 제공 할 수 있다고 생각했습니다. 그래서 그것이 내가 한 일입니다.
이제이 라이브러리를 사용하는 경우 나트륨 구현 또는 OpenSSL 구현을 사용할지 여부를 결정할 수 있습니다. 두 구현은 행복하게 공존 할 수 있기 때문에 원하는 경우 코드를 작성하여 코드로 이동할 수도 있습니다. 구현은 키 구성 또는 데이터 형식을 공유하지 않으며 완전히 분리됩니다. (즉, 암호화 알고리즘을 전환하는 것은 정확하지 않으며 모든 데이터를 마이그레이션하기 위해 오프라인으로 가야 할 것입니다. 그렇게 할 수 없다면 나트륨으로 시작하고 고집하지 않으면 알고리즘을 전환하지 마십시오.)
나는이 라이브러리가 내 자신의 암호화를 굴리는 것을 고려하지 않고 오히려 실제로 나트륨과 OpenSSL을 사용하는 방법을 알아내는 것으로 생각합니다. 내가 실수하거나 다른 실수를 저지른다면, 나는 그것에 대해 정말로 듣는 것에 감사 할 것입니다.
때때로 업데이트를 기억한다고 가정하면 여기에는 데모 시스템이 있습니다.
데모 시설은 HTML과 HTTP를 사용하여 클라이언트와 서버 간의 암호화 데이터를 왕복하는 방법을 보여줍니다.
데모 코드는 직접 호스팅하려면 SRC/ 데모/ 디렉토리 의이 라이브러리에서 사용할 수 있습니다.
때때로 업데이트를 기억한다고 가정하면 PHP 문서가 여기에 있습니다.
위에서 언급했듯이 다음과 같은 명령으로 Git의 코드를 확인할 수 있습니다.
git clone https://github.com/jj5/kickass-crypto.git
이 코드는 출시되지 않았으며 안정적인 버전이 없습니다.
응용 프로그램에 사용할 클라이언트 라이브러리를 포함하려면 Inc/Sodium.php 또는 Inc/Openssl.php 파일이 포함되어 있으며 다른 모든 것을로드 할 수 있습니다. 다음과 같은 것을 사용하십시오.
require_once __DIR__ . '/lib/kickass-crypto/inc/sodium.php';
이 라이브러리를로드 한 후 일반적으로 아래에 문서화 된 kickass_round_trip() 또는 kickass_at_rest() 서비스 로케이터를 통해 액세스 할 수 있습니다.
$ciphertext = kickass_round_trip()->encrypt( 'secret text' );
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
echo "the secret data is: $plaintext.n";
물건을 간단하게 만드는 데 많은 노력이 필요했습니다!
데모 코드를 호스팅하려면 SRC/ Demo/에서 파일을 호스팅해야하며 프로젝트 기반 디렉토리에 유효한 config.php 파일을 포함해야합니다 (이 readme.md 파일이 포함 된 디렉토리). 데모 목적의 경우 유효한 config.php 파일 만 CONFIG_SODIUM_SECRET_CURR 에 대한 상수 문자열 만 정의하면 다음과 같이 적절한 문자열을 생성 할 수 있습니다.
php bin/gen-key.php
또는 전체 데모 config.php 파일 만 생성 할 수 있습니다.
php bin/gen-demo-config.php > config.php
다음은 소프트웨어 파일 및 코드 줄에 대한 몇 가지 메모입니다.
Total Number of Files = 128
Total Number of Source Code Files = 128
| 예배 규칙서 | 파일 | 언어로 |
|---|---|---|
| 시험 | 63 | php = 59, sh = 4 |
| 암호 | 35 | PHP = 35 |
| 큰 상자 | 22 | php = 13, sh = 9 |
| Inc | 7 | PHP = 7 |
| 데모 | 1 | PHP = 1 |
| 언어 | 파일 | 백분율 |
|---|---|---|
| PHP | 115 | (89.84%) |
| 쉿 | 13 | (10.16%) |
Total Physical Source Lines of Code (SLOC) = 9,210
Development Effort Estimate, Person-Years (Person-Months) = 2.06 (24.70)
(Basic COCOMO model, Person-Months = 2.4 * (KSLOC**1.05))
Schedule Estimate, Years (Months) = 0.70 (8.46)
(Basic COCOMO model, Months = 2.5 * (person-months**0.38))
Estimated Average Number of Developers (Effort/Schedule) = 2.92
Total Estimated Cost to Develop = $ 278,044
(average salary = $56,286/year, overhead = 2.40).
| 예배 규칙서 | SLOC | 언어로 |
|---|---|---|
| 암호 | 5,136 | PHP = 5136 |
| 시험 | 3,363 | PHP = 3193, SH = 170 |
| 큰 상자 | 603 | PHP = 423, SH = 180 |
| 데모 | 71 | PHP = 71 |
| Inc | 37 | PHP = 37 |
| 언어 | SLOC | 백분율 |
|---|---|---|
| PHP | 8,860 | (96.20%) |
| 쉿 | 350 | (3.80%) |
이 코드는 PHP 7.4 이상에서 작동해야합니다. 이전 버전의 PHP 에서이 코드를 실행하려고하면 오류 메시지를 기록한 다음 프로세스를 종료하려고합니다.
이 코드는 64 비트 플랫폼에서 실행 중인지 확인합니다. 그렇지 않으면 불평하고 종료 할 것입니다.
나트륨 모듈을로드하면 라이브러리는 나트륨 라이브러리를 실제로 사용할 수 있도록합니다. 그렇지 않은 경우 프로세스가 불평하고 종료됩니다.
OpenSSL 모듈을로드하면 라이브러리는 OpenSSL 라이브러리를 실제로 사용할 수 있는지 확인합니다. 그렇지 않은 경우 프로세스가 불평하고 종료됩니다.
이 코드는 모든 운영 체제에서 실행되어야한다고 생각하지만 Linux에서만 테스트했습니다. MacOS 또는 Windows에서 성공했다면 기꺼이들을 것입니다.
쉘 스크립트는 Bash 용으로 작성되었습니다. 배쉬가없는 경우 포트가 필요할 수 있습니다.
이 코드는 두 가지 특정 사용 사례를 지원합니다.
키는 각 사용 사례에 대해 별도로 다르게 관리됩니다.
각 유스 케이스가 지원되는 방법에 대한 세부 사항은 아래에 문서화되어 있습니다.
이 라이브러리를 레스트 암 암호화에 사용하는 것은 일반적으로 왕복 암호화에 단순히 사용하는 것보다 더 큰 위험과 더 큰 헌신입니다. 왕복 암호화 키를 잃어 버리거나 긴급하게 회전 해야하는 경우 긴급하게 회전해야합니다.
이 라이브러리가 개발 된 주요 사용 사례는 낙관적 동시성 제어를위한 약간 민감하지만 미션 크리티컬 행 버전 번호를 포함하지 않는 몇 킬로바이트의 데이터를 왕복하는 것이 었습니다. 대안과 비교하여 (낙관적 동시성 제어 데이터를 암호화하거나 훼손하지 않음)이 라이브러리의 사용은 개선입니다. 다른 응용 프로그램에서 실제로 적합한 지 여부는 공개적인 질문이지만 확실하지 않습니다. 확실히 필요한 보안 수준을 제공하지 않으면이 라이브러리를 사용해서는 안됩니다.
구성 파일에서 비밀을 지명하는 선호 및 지원되는 방법은 php define () 함수를 사용하는 상수입니다. 클래스/인스턴스 필드 또는 글로벌 변수를 사용하는 데있어 문제는 값이 디버그 및 로깅 코드로 상당히 쉽게 누출 될 수 있다는 것입니다. 마찬가지로 글로벌/정적 데이터를 캐시 해야하는 경우 (구성 파일에서 읽은 것과 같은) 수행하는 가장 좋은 방법은 인스턴스 필드, 클래스 필드 또는 글로벌을 사용하면 비밀 누출로 이어질 수있는 기능의 로컬 정적 변수를 사용하는 것입니다.
예를 들기 위해 double-define.php 라는 테스트 파일을 작성하겠습니다.
<?php
define( 'TEST', 123 );
define( 'TEST', 456 );
그런 다음 코드를 실행하면 이와 같은 일이 발생합니다.
$ php double-define.php
PHP Warning: Constant TEST already defined in ./double-define.php on line 4
PHP Stack trace:
PHP 1. {main}() ./double-define.php:0
PHP 2. define($constant_name = 'TEST', $value = 456) ./double-define.php:4
그 상수 가치에 비밀 키가 포함되어 있다면, 당신은 아주 나쁜 하루를 보냈습니다.
PHP에서 상수를 정의하는 가장 안전한 방법은 이미 정의 된 상수를 정의하려고 시도하면 오류가 발생하기 때문에 아직 먼저 정의되지 않은지 확인하는 것입니다. 이미 정의 된 상수를 찾으면 오류 메시지로 중단 할 수 있습니다 (공개 웹에서 볼 수 있기 때문에 너무 많은 세부 사항을 제공하지 않는 경우) 또는 기존 값을 유지하고 재정의하려고 시도하지 마십시오. BIN/GEN-DEMO-CONFIG.PHP 구성 파일 생성기는 첫 번째 접근 방식을 취하고 중복을 감지하면 php die() 함수를 호출합니다. 생성 된 config.php 파일을 두 번 포함시켜 다음과 같이 어떻게되는지 볼 수 있습니다.
require __DIR__ . '/config.php';
require __DIR__ . '/config.php';
config-die.php에 config.php 두 배로 포함 시키면 어떻게되는지에 대한 예를 찾을 수 있습니다.
결과적으로 대부분의 PHP 소스 파일과 마찬가지로 config.php 파일을 포함시킬 때 require_once 사용하는 것이 가장 좋습니다.
require_once __DIR__ . '/config.php';
비밀의 이름을 지정할 때 이름에 문자열 "Pass"( "비밀번호", "Passwd"및 "PassPhrase"또는 "Passport") 또는 "Secret"에서 문자열이 포함되어 있는지 확인하십시오. 내 범용 로깅 시설 (이 라이브러리에 포함되지 않음)에서 진단 데이터를 기록하기 전에 일치하는 이름 (Case Insensentitive)으로 모든 것을 문지르고 수정합니다. 이 관행을 채택하는 것이 좋습니다.
이 라이브러리에서 변수 또는 상수에 민감한 데이터가 포함될 수있는 경우 이름의 하위 문자로 "Pass"또는 "Secret"으로 명명됩니다.
민감한 데이터를 로그에 쓰지 마십시오.
민감한 변수, 필드 또는 상수의 이름으로 'Pass'또는 'Secret'을 넣으십시오.
여기서 나는이 비슷한 소리의 용어가 실제로이 라이브러리의 맥락에서 무엇을 의미하는지 설명합니다.
기본 모듈을 사용하면 데이터 형식은 OpenSSL 모듈의 경우 "KA0"또는 나트륨 모듈의 경우 "KAS0"입니다.
기본 프레임 워크를 상속하고 자신의 암호화 모듈을 정의하면 기본 데이터 형식은 나트륨 구현을 기반으로 OpenSSL 구현 또는 모듈의 모듈에 대한 모듈의 "XKA0"입니다. 그렇지 않으면 do_get_const_data_format() 의 구현이 무엇인지에 대한 정보를 제공 할 수있는 한, "KA"를 결정할 수 있습니다. 구현.
암호 텍스트를 성공적으로 해독하려면 데이터 형식에 적합한 모듈을 사용해야합니다.
데이터 인코딩은 JSON, PHP 직렬화 또는 텍스트입니다. 데이터 형식 (위)에 대한 올바른 모듈이 있다고 가정하고 아래에 논의 된 경고를 사용하면 사용한 데이터 인코딩에 관계없이 모든 것을 해독 할 수 있습니다. 구성된 데이터 인코딩을 사용하여 암호화가 수행됩니다. config_encryption_data_encoding을 참조하십시오.
config_encryption_phps_enable도 정의하지 않으면 PHPS 인코딩을 사용할 수 없습니다. PHP 사막화가 안전하지 않아서 기본적으로 비활성화 될 수 있기 때문입니다. 솔직히 이것은 약간 손으로 물결되어 있습니다. 방금 PHP unserialize() 코드 주입으로 이어질 수 있다는 소문을 들었지만 그것이 사실인지 정확히 무엇을 의미하는지 확실하지 않습니다. PHP 직렬화 및 사제화를 구현하고 약간의 테스트를했지만 실제로 불안한 지 모르겠습니다. JSON과 텍스트 데이터 인코딩이 안전해야한다고 확신합니다.
KickassCrypto 의 상속 및 특정 기능을 재정의하는 것 외에도 구성 상수를 통해 많은 구성을 사용할 수 있습니다. CONFIG_SODIUM 검색하여 나트륨 및 CONFIG_OPENSSL 에서 사용할 수있는 항목을 찾아서 OpenSSL에 사용할 수있는 것을 찾으십시오.
현재이 코드는 config.php 파일에서 직접 구성됩니다.
향후 config.php 파일에는 별도로 관리되는 구성 파일이 포함됩니다.
이 파일에서 키를 자동으로 회전하고 프로비저닝하기위한 관리 스크립트가 있습니다.
숙련 된 Linux 사용자는 /etc/sudoers 직접 편집하지 않는다는 것을 알고, visudo 로 편집하여 실수로 구문 오류를 도입하지 않았는지 확인하고 시스템을 호출했는지 확인할 수 있습니다.
config.php 및 기타 구성 파일을 편집하고 관리하기위한 유사한 스크립트를 제공하려고합니다. 그러한 업데이트에 대기하십시오. 그 동안 ... 그냥 매우 조심하십시오 .
당신이하지 말아야 할 한 가지는 ".php"파일 확장자가있는 PHP 파일 이외의 다른 것에서 키를 관리하는 것입니다. 키를 ".ini"파일에 넣거나 웹 서버에서 일반 텍스트로 잘 제공 될 수 있습니다 . 그러니 그렇게하지 마십시오. 또한 세부 사항이 잠재적 인 결과 오류 메시지와 함께 유출 될 수 있으므로 구성 파일 또는 프로덕션에서 실행중인 다른 소스 파일에 구문 오류를 도입하지 않도록주의하십시오.
이전 섹션에서 언급했듯이, 명명 된 구성 상수에 대한 지원에 의해 상당한 양의 구성 가능성이 제공됩니다.
구성 상수 외에도 KickassCrypto 기본 클래스에서 상속하고 그 방법을 무시하면 할 수있는 일이 많이 있습니다.
구성 상수에 대한 대안으로 (프로세스 당 한 번만 정의 할 수 있고 그 후에 만 변경할 수 없음) 구성 옵션에 대한 get_config_...() 와 같은 인스턴스 메소드 및 일정한 평가를위한 get_const_...() 와 같은 인스턴스 방법이 있습니다. 가장 중요한 상수와 구성 옵션은 이러한 액세서를 통해 간접적으로 읽으므로 안정적으로 무시할 수 있어야합니다.
PHP 내장 기능에 대한 대부분의 호출은 KickassCrypto 의 보호 기능을 통해 얇은 포장지에 의해 수행됩니다. 이들은 KICKASS_WRAPPER_PHP 특성에 정의되어 있습니다. 이 간접적으로 특정 PHP 함수 호출을 가로 채고 잠재적으로 수정할 수 있습니다. 이는 주로 단위 테스트 중에 결함 주입을 지원하기 위해 수행되었지만 구현 세부 사항을 변경하기 위해 다른 목적으로 사용할 수 있습니다.
KickassCrypto 에서 민감한 것으로 간주되는 것은 개인 또는 최종 으로 정의됩니다. 비공개가 아니고 최종적이지 않다면 (실수하지 않는 한) 재정의 공정한 게임입니다. 특히 do_ 로 시작하는 인스턴스 방법은 구체적으로 구현자가 교체하거나 가로 채도록 만들어졌습니다.
이 라이브러리는 각각 암호화 라이브러리의 인스턴스를 관리하는 두 가지 서비스 로케이터 기능을 제공합니다.
kickass_round_trip()kickass_at_rest()기능을 호출하고 새 인스턴스를 유일한 매개 변수로 전달하여 서비스 로케이터 기능이 제공하는 서비스 인스턴스를 다음과 같이 대체 할 수 있습니다.
class MyKickassCrypto extends KickassCryptoKickassCrypto {
protected function do_is_valid_config( &$problem = null ) { return TODO; }
protected function do_get_passphrase_list() { return TODO; }
// ... other function overrides ...
}
kickass_round_trip( new MyKickassCrypto );
이상적 으로이 라이브러리는 상자에서 요구 사항을 충족하거나 특정 구성을 사용하여 서비스 로케이터가 제공 한 인스턴스를 기본적으로 교체 할 필요가 없습니다.
서비스 로케이터는 인스턴스가없는 경우 서비스 로케이터에 대한 첫 번째 호출에서 새로운 기본 인스턴스를 생성합니다. 기본 구현이 나트륨 모듈인지 OpenSSL 모듈인지 여부는 inc/sodium.php 및 inc/openssl.php 파일을 포함한 순서에 따라 다릅니다. inc/library.php 에 전체 라이브러리를 포함 시키면 나트륨 모듈이 우선합니다.
나트륨 모듈 또는 OpenSSL 모듈의 서비스 로케이터를로드했는지 여부에 관계없이 새 인스턴스로 서비스 로케이터를 호출하여 인수로 기본 인스턴스를 대체 할 수 있습니다.
암호화 프로세스는 대략입니다.
나트륨 라이브러리는 초기화 벡터 (유사한 효과) 대신 Nonce를 사용하고 나트륨은 자체 인증 태그를 처리합니다.
이 라이브러리가 암호 텍스트를 인코딩하면 나트륨 구현 용 "KAS0/"의 데이터 형식 접두사와 OpenSSL 구현의 경우 "KA0/"가 포함됩니다.
데이터 형식 접두사의 Zero ( "0")는 버전 0 에 대한 것이며, 이는 인터페이스가 불안정하고 변경 될 수 있음 을 암시합니다.
이 라이브러리의 향후 버전은 안정적인 데이터 형식에 대한 새로운 데이터 형식 접두사를 구현할 수 있습니다.
이 라이브러리가 암호 텍스트를 디코딩하면 데이터 형식 접두사를 확인합니다. 현재 "KAS0/"또는 "KA0/"만 지원됩니다.
위에서 언급 한 버전 제로 데이터 형식은 현재 다음을 의미합니다.
데이터 인코딩 후 (기본적으로 JSON, 다음 섹션에서 논의 됨) 패딩이 완료되고 데이터 길이가 접두사됩니다. 암호화 전에 다음과 같이 메시지가 형식화됩니다.
$message = $encoded_data_length . '|json|' . $encoded_data . $this->get_padding( $pad_length );
JSON 데이터 길이는 8 자 66 진수 값으로 형식화됩니다. 8 자의 크기는 일정하며 JSON 데이터 길이의 크기에 따라 달라지지 않습니다.
패딩의 이유는 실제 데이터 크기를 가리기 때문입니다. 패딩은 최대 4 개의 kib 경계 (2 12 바이트)로 수행되며, 이는 청크라고합니다. 청크 크기는 구성 가능하며 기본값은 향후 변경 될 수 있습니다.
그런 다음 나트륨으로 암호화하는 경우 메시지는 sodium_crypto_secretbox() 로 암호화되고 Nonce와 Ciphertext가 다음과 같이 함께 연결됩니다.
$nonce . $ciphertext
그렇지 않으면 OpenSSL로 암호화하는 경우 메시지는 AES-256-GCM으로 암호화되며 초기화 벡터, 암호 텍스트 및 인증 태그가 다음과 같이 연결됩니다.
$iv . $ciphertext . $tag
그런 다음 모든 것이 Php Base64_encode () 함수로 인코딩 된 Base64이며 데이터 형식 접두사가 추가됩니다.
다음과 같이 수행되는 나트륨의 경우 :
"KAS0/" . base64_encode( $nonce . $ciphertext )
그리고 다음과 같이 수행되는 OpenSSL의 경우 :
"KA0/" . base64_encode( $iv . $ciphertext . $tag )
암호 해독 프로세스는 "KAS0"데이터 형식에 대한 24 바이트 Nonce 및 Ciphertext를 찾을 것으로 예상합니다. 12 바이트 초기화 벡터, 암호 텍스트 및 KA0 데이터 형식의 16 바이트 인증 태그를 찾을 수 있습니다.
암호 텍스트를 해독 한 후 라이브러리는 JSON 데이터의 크기를 8 문자 16 진수 인코딩 된 값을 나타내는 ASCII 문자열로, 단일 파이프 문자를 나타내고, 4 개의 문자 데이터 인코딩 표시기 ( 'JSON'또는 'PHPS')를 찾은 다음 JSON (또는 PHP 시리얼 화 된 데이터)이 뒤 따릅니다. 그런 다음 라이브러리는 패딩에서 JSON/직렬화 된 데이터를 추출하고 나머지 디코딩을 처리 할 수 있습니다.
암호화 이전 입력 데이터는 php json_encode () 함수를 사용하여 JSON으로 인코딩됩니다. 처음 에이 라이브러리는 PHP Serialize () 함수를 사용했지만 일부 코드 실행 시나리오로 이어질 수 있으므로 (세부 사항에 대해서는 확실하지 않음) JSON 인코딩이 더 안전하다고 결정되었습니다. 따라서 이제 우리는 대신 JSON 인코딩을 사용합니다.
데이터 인코딩 형식으로 JSON을 사용하면 지원할 수있는 값에 대해 약간의 영향을 미칩니다. 특히 우리는 나중에 객체 인스턴스로 다시 디코딩 할 수있는 객체 인스턴스를 인코딩 할 수 없습니다 (객체가 jsonserializable 인터페이스를 구현하는 경우 데이터로 직렬화 될 수 있지만, 그 온 PHP 객체가 아니라 PHP 배열로 다시 디코딩됩니다). 일부 홀수 부동 소수점 값은 표현 될 수 없습니다 (예 : NAN, POS INF, NEG INFO 및 NEG ZERO). 이진 문자열은 JSON에서 표현할 수 없습니다.
기본적 으로이 옵션은 JSON 인코딩에 사용됩니다.
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
그러나 이러한 옵션은 JSON을 해독하는 구현 능력에 영향을 미치지 않습니다. 구현은 data_encode () 및 data_decode () 메소드를 재정의하여 필요한 경우 JSON 인코딩 및 디코딩을 미세 조정할 수 있습니다. 또는 CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS 및 CONFIG_ENCRYPTION_JSON_DECODE_OPTIONS 상수를 사용하여 config.php 파일에서 JSON 인코딩 및 디코딩 옵션을 지명 할 수 있습니다.
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_THROW_ON_ERROR );
이 라이브러리는 JSON_THROW_ON_ERROR 지정되었는지 여부에 관계없이 작동해야합니다.
JSON 인코딩 옵션에서 JSON_PARTIAL_OUTPUT_ON_ERROR 지정하면 데이터가 무효화 될 수 있으므로 자신의 위험에 따라 그렇게하십시오. 아마도 직관적으로 나는 JSON_PARTIAL_OUTPUT_ON_ERROR 가능하게하는 것이 최소한 최악 의 전략이라는 것을 알았을 것입니다. 입력의 일부 부분을 인코딩 할 수없는 경우 JSON_PARTIAL_OUTPUT_ON_ERROR 활성화하지 않으면 (UTF-8과 같은 유효한 인코딩에없는 이진 문자열이있을 때) 데이터 전체가 제거됩니다. JSON_PARTIAL_OUTPUT_ON_ERROR 에서는 대표 할 수없는 부분 만 생략됩니다. 현재 JSON_PARTIAL_OUTPUT_ON_ERROR 자동으로 지정되지 않았지만 이것은 앞으로 다시 방문 할 수있는 것입니다.
이러한 JSON 인코딩/디코딩 옵션을 사용하는 경우 시간이 많이 걸릴 수 있습니다.
JSON_NUMERIC_CHECKJSON_INVALID_UTF8_IGNOREJSON_INVALID_UTF8_SUBSTITUTE 이 라이브러리가 데이터를 암호화하면 출력을 구성 가능한 청크 크기로 패드합니다.
청크 크기의 구성 상수는 CONFIG_ENCRYPTION_CHUNK_SIZE 입니다.
기본 청크 크기는 4,096 (2 12 )입니다.
청크 크기를 8,192로 늘리려면 다음과 같은 config.php 파일에서이를 수행 할 수 있습니다.
define( 'CONFIG_ENCRYPTION_CHUNK_SIZE', 8912 );
정의 된 청크 크기를 변경할 수 있으며 새 데이터에 적용되기 시작하며 다른 청크 크기로 암호화 된 오래된 데이터는 여전히 해독 될 수 있습니다.
데이터 크기 한계가 관찰되는 한 (이들은 다음에 논의 됨),이 라이브러리는 PHP에 의해 JSON으로 인코딩 할 수있는 모든 것을 암호화 할 수 있습니다.
여기에는 다음과 같은 다양한 것들이 포함됩니다.
JSON으로 지원할 수없는 것 :
부울 값 False는 암호화 할 수 없습니다. 우리가 암호화 할 수 없었기 때문이 아니라, 암호 해독이 실패했을 때 반환하기 때문입니다. 그래서 우리는 암호 해독시 오류와 혼동 될 수 없도록 거짓을 암호화하지 않습니다.
부울 값을 암호화 해야하는 경우 오 탐지를 고려하십시오.
$input = [ 'value' => false ];
또는 JSON과 같은 인코딩 :
$input = json_encode( false );
당신이 그 일 중 하나를 수행하면 가치를 암호화 할 수 있습니다.
PHP "문자열"에서 본질적으로 바이트 어레이라는 점을 지적하는 것이 좋습니다. 즉, 본질적으로 "이진"데이터를 포함 할 수 있습니다. 그러나 이러한 이진 데이터는 JSON으로 표시 될 수 없습니다. 이진 데이터를 처리 해야하는 경우 가장 좋은 방법은 아마도 base64_encode ()를 사용하여 base64로 인코딩하거나 bin2Hex ()를 사용하여 암호화하는 것입니다.
향후 JSON 인코딩되지 않은 데이터로 작업 할 수있는 기능 이이 라이브러리에 추가 될 수 있습니다. 그것이 당신이 가지고있는 기능인지 알려주세요.
참고 : JSON 인코딩 대신 PHP 직렬화를 사용하는 것이 이제 옵션입니다. 이 문서는 작동 방식 및 사용 방법을 설명하기 위해 업데이트되어야합니다. PHP 직렬화의 장점은 JSON이 할 수있는 것보다 더 많은 데이터 유형과 형식을 지원한다는 것입니다.
데이터가 JSON으로 인코딩 된 후 구성 가능한 최대 길이로 제한됩니다.
최대 JSON 인코딩 길이에 대한 구성 상수는 CONFIG_ENCRYPTION_DATA_LENGTH_MAX 입니다.
기본 데이터 인코딩 한계는 67,108,864 (2^ 26 ) 바이트이며 약 67MB 또는 64 MIB입니다.
더 크거나 작게 만들어야하는 경우이 데이터 인코딩 한계를 구성 할 수 있습니다. 한계를 너무 크게 만들면 메모리 문제로 끝나고 프로세스가 종료 될 수 있습니다.
데이터 인코딩 한계를 줄이려면 config.php 파일에서 다음과 같이 수행 할 수 있습니다.
define( 'CONFIG_ENCRYPTION_DATA_LENGTH_MAX', pow( 2, 25 ) );
이 라이브러리는 입력 데이터를 압축 하지 않습니다 . 압축은 범죄 SSL/TLS 공격과 같은 암호화 약점을 도입 할 수 있기 때문입니다.
문제는 공격자가 일반 텍스트의 일부를 수정할 수 있다면 일반 텍스트의 다른 부분에 입력하는 데이터가 존재하는지 알 수 있습니다. 값을 넣고 결과가 더 작기 때문에 일반 텍스트의 일부에 존재하기 때문입니다.
공격자가 비밀 인 다른 데이터를 제공 할 수있는 데이터를 압축하지 않는 것이 매우 중요합니다. 전혀 압축하지 않는 것이 가장 좋습니다.
암호화 또는 암호 해독 중에 오류가 발생하면 1 밀리 초 (1ms)와 10 초 (10 초)의 지연이 도입됩니다. 이것은 잠재적 인 타이밍 공격에 대한 완화입니다. 토론은 S2N 및 Lucky 13을 참조하십시오.
Note that avoiding timing attacks is hard. A malicious guest on your VPS host (or a malicious person listening to your server's fans! ?) could figure out that your process is sleeping rather than doing actual work.
This library includes a method called delay() , and this method is called automatically on the first instance of an error. The delay() method does what is says on the tin: it injects a random delay into the process. The delay() method is public and you can call it yourself if you feel the need. Each time delay() is called it will sleep for a random amount of time between 1 millisecond and 10 seconds.
The programmer using this library has the opportunity to override the do_delay() method and provide their own delay logic.
If that do_delay() override throws an exception it will be handled and an emergency delay will be injected.
If you do override do_delay() but don't actually delay for at least the minimum duration (which is 1 ms) then the library will inject the emergency delay.
The main reason for allowing the implementer to customize the delay logic is so that unit tests can delay for a minimum amount of time. Ordinarily there shouldn't be any reason to meddle with the delay logic and it might be less safe to do so.
When an instance of one of of the following is created the configuration settings are validated.
KickassSodiumRoundTripKickassSodiumAtRestKickassOpenSSLRoundTripKickassOpenSSLAtRestIf the configuration settings are not valid the constructor will throw an exception. If the constructor succeeds then encryption and decryption later on should also (usually) succeed. If there are any configuration problems that will mean encryption or decryption won't be able to succeed (such as secret keys not having been provided) the constructor should throw.
This library defines its own exception class called KickassException . This works like a normal Exception except that it adds a method getData() which can return any data associated with the exception. A KickassException doesn't always have associated data.
Of course not all problems will be able to be diagnosed in advance. If the library can't complete an encryption or decryption operation after a successful construction it will signal the error by returning the boolean value false. Returning false on error is a PHP idiom, and we use this idiom rather than raising an exception to limit the possibility of an exception being thrown while an encryption secret or passphrase is on the call stack.
The problem with having sensitive data on the call stack when an exception is raised is that the data can be copied into stack traces, which can get saved, serialized, displayed to users, logged, etc. We don't want that so we try very hard not to raise exceptions while sensitive data might be on the stack.
If false is returned on error, one or more error messages will be added to an internal list of errors. The caller can get the latest error by calling the method get_error . If you want the full list of errors, call get_error_list .
If there were any errors registered by the OpenSSL library functions (which the OpenSSL module calls to do the heavy lifting), then the last such error is available if you call the get_openssl_error() . You can clear the current error list (and OpenSSL error message) by calling the method clear_error() .
For the PHP Sodium implementation the function we use is sodium_crypto_secretbox(). That's XSalsa20 stream cipher encryption with Poly1305 MAC authentication and integrity checking.
For the PHP OpenSSL implementation the cipher suite we use is AES-256-GCM. That's Advanced Encryption Standard encryption with Galois/Counter Mode authentication and integrity checking.
Secret keys are the secret values you keep in your config.php file which will be processed and turned into passphrases for use by the Sodium and OpenSSL library functions. This library automatically handles converting secret keys into passphrases so your only responsibility is to nominate the secret keys.
The secret keys used vary based on the use case and the module. There are two default use cases, known as round-trip and at-rest.
The "256" in AES-256-GCM means that this cipher suite expects 256-bit (32 byte) passphrases. The Sodium library sodium_crypto_secretbox() function also expects a 256-bit (32 byte) passphrase.
We use a hash algorithm to convert our secret keys into 256-bit binary strings which can be used as the passphrases the cipher algorithms expect.
The minimum secret key length required is 88 bytes. When these keys are generated by this library they are generated with 66 bytes of random data which is then base64 encoded.
The secret key hashing algorithm we use is SHA512/256. That's 256-bits worth of data taken from the SHA512 hash of the secret key. When this hash code is applied with raw binary output from an 88 byte base64 encoded input you should be getting about 32 bytes of randomness for your keys.
The Sodium library expects to be provided with a nonce, in lieu of an initialization vector.
To understand what problem the nonce mitigates, think about what would happen if you were encrypting people's birthday. If you had two users with the same birthday and you encrypted those birthdays with the same key, then both users would have the same ciphertext for their birthdays. When this happens you can see who has the same birthday, even when you might not know exactly when it is. The initialization vector avoids this potential problem.
Our AES-256-GCM cipher suite supports the use of a 12 byte initialization vector, which we provide. The initialization vector ensures that even if you encrypt the same values with the same passphrase the resultant ciphertext still varies.
This mitigates the same problem as the Sodium nonce.
Our AES-256-GCM cipher suite supports the validation of a 16 byte authentication tag.
The "GCM" in AES-256-GCM stands for Galois/Counter Mode. The GCM is a Message Authentication Code (MAC) similar to a Hash-based Message Authentication Code (HMAC) which you may have heard of before. The goal of the GCM authentication tag is to make your encrypted data tamperproof.
The Sodium library also uses an authentication tag but it takes care of that by itself, it's not something we have to manage. When you parse_binary() in the Sodium module the tag is set to false.
This library requires secure random data inputs for various purposes:
There are two main options for generating suitable random data in PHP, those are:
Both are reasonable choices but this library uses random_bytes().
If the random_bytes() function is unable to generate secure random data it will throw an exception. See the documentation for details.
We also use the PHP random_int() function to generate a random delay for use in timing attack mitigation.
The round-trip use case is for when you want to send data to the client in hidden HTML form <input> elements and have it POSTed back later.
This use case is supported with two types of secret key.
The first key is called the current key and it is required.
The second key is called the previous key and it is optional.
Data is always encrypted with the current key.
Data is decrypted with the current key, and if that fails it is decrypted with the previous key. If decryption with the previous key also fails then the data cannot be decrypted, in that case the boolean value false will be returned to signal the error.
When you rotate your round-trip secret keys you copy the current key into the previous key, replacing the old previous key, and then you generate a new current key.
The config setting for the current key for the Sodium module is: CONFIG_SODIUM_SECRET_CURR .
The config setting for the current key for the OpenSSL module is: CONFIG_OPENSSL_SECRET_CURR .
The config setting for the previous key for the Sodium module is: CONFIG_SODIUM_SECRET_PREV .
The config setting for the previous key for the OpenSSL module is: CONFIG_OPENSSL_SECRET_PREV .
To encrypt round-trip data:
$ciphertext = kickass_round_trip()->encrypt( 'secret data' );
To decrypt round-trip data:
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
The at-rest use case if for when you want to encrypt data for storage in a database or elsewhere.
This use case is supported with an arbitrarily long list of secret keys.
The list must include at least one value. The first value in the list is used for encryption. For decryption each secret key in the list is tried until one is found that works. If none work the data cannot be decrypted and the boolean value false is returned to signal the error.
When you rotate your at-rest secret keys you add a new master key as the first item in the list. You need to keep at least one extra key, and you can keep as many in addition to that as suits your purposes.
After you rotate your at-rest secret keys you should consider re-encrypting all your existing at-rest data so that it is using the latest key. After you have re-encrypted your at-rest data, you can remove the older key.
The config setting for the key list for the Sodium module is: CONFIG_SODIUM_SECRET_LIST .
The config setting for the key list for the OpenSSL module is: CONFIG_OPENSSL_SECRET_LIST .
Please be aware: if you restore an old backup of your database, you will also need to restore your old keys.
Be very careful that you don't lose your at-rest secret keys. If you lose these keys you won't be able to decrypt your at-rest data.
To encrypt at-rest data:
$ciphertext = kickass_at_rest()->encrypt( 'secret data' );
To decrypt at-test data:
$plaintext = kickass_at_rest()->decrypt( $ciphertext );
It has been noted that key management is the hardest part of cybersecurity. This library can't help you with that.
Your encrypted data is only as secure as the secret keys.
If someone gets a copy of your secret keys, they will be able to decrypt your data.
If someone gets a copy of your encrypted data now, they can keep it and decrypt it if they get a copy of your secret keys in the future. So your keys don't have to be only secret now, but they have to be secret for all time.
If you lose your secret keys, you won't be able to decrypt your data.
Your round-trip data is probably less essential than your at-rest data.
It's a very good idea to make sure you have backups of the secret keys for your essential round-trip or at-rest data. You can consider:
When doing key management it is important to make sure your config files are edited in a secure way. A syntax error in a config file could lead to a secret key being exposed to the public web. If this happened you would have to rotate all of your keys immediately and then destroy the old compromised keys, even then it might be too late .
It would be a good idea to stand ready to do a key rotation in an automated and tested fashion immediately in case of emergency.
When you rotate your round-trip and at-rest keys you need to make sure they are synchronized across all of your web servers.
I intend to implement some facilities to help with key deployment and config file editing but those facilities are not done yet.
This library supports encrypted data at-rest, and encrypted data round-trips. Another consideration is data in motion. Data in motion is also sometimes called data in transit.
Data is in motion when it moves between your web servers and your database server. Data is also in motion when it moves between your web servers and the clients that access them. You should use asymmetric encryption for your data in motion. Use SSL encryption support when you connect to your database, and use HTTPS for your web clients.
This library is a server-side component. We don't support encrypting data client-side in web browsers.
This library collects some basic telemetry:
Call KickassCrypto::GetTelemetry() to get the telemetry and KickassCrypto::ReportTelemetry() to report it.
The unit tests are in the src/test/ directory, numbered sequentially.
There's some test runners in bin/dev/, as you can see. Read the scripts for the gory details but in brief:
There are also some silly tests, but we won't talk about those. They are not ordinarily run. And they're silly.
If you want to add a normal/fast test create the unit test directory as src/test/test-XXX , then add either fast.php or fast.sh . If you create both then fast.sh will have precedence and fast.php will be ignored.
If you want to add a slow test create the unit test directory as src/test/test-XXX , then add either slow.php or slow.sh . If you create both then slow.sh will have precedence and slow.php will be ignored.
You usually only need to supply a shell script if your unit tests require multiple processes to work. That can happen when you need to test different constant definitions. As you can't redefine constants in PHP you have to restart your process if you want to run with different values.
See existing unit tests for examples of how to use the simple unit test host.
I have heard of and used PHPUnit (although I haven't used it for a long while). I don't use it in this project because I don't feel I need it or that it adds much value. Tests are a shell script, if that's missing they're a PHP script. If I need to make assertions I call assert(). 쉬운.
Here are some notes about the various idioms and approaches taken in this library.
In the code you will see things like this:
protected final function is_valid_settings( int $setting_a, string $setting_b ) : bool {
if ( strlen( $setting_b ) > 20 ) { return false; }
return $this->do_is_valid_settings( $setting_a, $setting_b );
}
protected function do_is_valid_settings( $setting_a, $setting_b ) {
if ( $setting_a < 100 ) { return false; }
if ( strlen( $setting_b ) > 10 ) { return false; }
return 1;
}
There are several things to note about this idiom.
In talking about the above code we will call the first function is_valid_settings() the "final wrapper" (or sometimes the "main function') and we call the second function do_is_valid_settings() the "default implementation".
The first thing to note is that the final wrapper is_valid_settings() is declared final and thus cannot be overridden by implementations; and the second thing to note is that the final wrapper declares the data types on its interface.
In contrast the default implementation do_is_valid_settings() is not marked as final, and it does not declare the types on its interface.
This is an example of Postel's Law, which is also known as the Robustness Principle. The final wrapper is liberal in what it accepts, such as with the return value one ( 1 ) from the default implementation; and conservative in what it does, such as always returning a properly typed boolean value and always providing values of the correct type to the default implementation.
Not needing to write out and declare the types on the interface of the default implementation also makes implementation and debugging easier, as there's less code to write. (Also I find the syntax for return types a bit ugly and have a preference for avoiding it when possible, but that's a trivial matter.)
Ordinarily users of this code will only call the main function is_valid_settings() , and anyone implementing new code only needs to override do_is_valid_settings() .
In general you should always wrap any non-final methods (except for private ones) with a final method per this idiom, so that you can have callers override functionality as they may want to do but retain the ability to maintain standards as you may want to do.
If you're refactoring a private method to make it public or protected be sure to introduce the associated final wrapper.
One last thing: if your component has a public function, it should probably be a final wrapper and just defer to a default implementation.
Default implementations should pretty much always be protected, certainly not public, and maybe private if you're not ready to expose the implementation yet.
Having types on the interface of the final method is_valid_settings() confers three main advantages.
The first is that the interface is strongly typed, which means your callers can know what to expect and PHP can take care of fixing up some of the smaller details for us.
The second advantage of this approach is that our final wrapper function is marked as final. This means that the implementer can maintain particular standards within the library and be assured that those standards haven't been elided, accidentally or otherwise.
Having code that you rely on marked as final helps you to reason about the possible states of your component. In the example given above the requirement that $setting_b is less than or equal to 20 bytes in length is a requirement that cannot be changed by implementations; implementations can only make the requirements stronger, such as is done in the default implementation given in the example, where the maximum length is reduced further to 10 bytes.
Another advantage of the typed interface is that it provides extra information which can be automatically added into the documentation. The typed interface communicates intent to the PHP run-time but also to other programmers reading, using, or maintaining the code.
Not having types on the interface of the default implementation do_is_valid_settings() confers four main advantages.
The first is that it's easier to type out and maintain the overriding function as you don't need to worry about writing out the types.
Also, in future, the is_valid_settings() might declare a new interface and change its types. If this happens it can maintain support for both old and new do_is_valid_settings() implementations without implementers necessarily needing to update their code.
The third advantage of an untyped interface for the do_is_valid_settings() function is that it allows for the injection of "impossible" values. These are values which will never be able to make it past the types declared on the main function is_valid_settings() and into the do_is_valid_settings() function, and being able to inject such "impossible" values can make unit testing of particular situations easier, as you can pass in a value that could never possibly occur in production in order to signal something from the test in question.
The fourth and perhaps most important implication of the approach to the default implementation is that it is not marked as final which means that programmers inheriting from your class can provide a new implementation, thereby replacing, or augmenting, the default implementation.
One way a programmer can go wrong is to infinitely recurse. For example like this:
class InfiniteRecursion extends KickassCryptoOpenSslKickassOpenSslRoundTrip {
protected function do_encrypt( $input ) {
return $this->encrypt( $input );
}
}
If the do_encrypt() function calls the encrypt() function, the encrypt() function will call the do_encrypt() function, and then off we go to infinity.
If you do this and you have Xdebug installed and enabled that will limit the call depth to 256 by default. If you don't have Xdebug installed and enabled PHP will just start recurring and will continue to do so until it hits its memory limit or runs out of RAM.
Since there's pretty much nothing this library can do to stop programmers from accidentally writing code like the above what we do is to detect when it's probably happened by tracking how deep our calls are nested using an enter/leave discipline, like this:
try {
$this->enter( __FUNCTION__ );
// 2023-04-07 jj5 - do work...
return $result;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
}
finally {
try { $this->leave( __FUNCTION__ ); } catch ( Throwable $ignore ) { ; }
}
The leave() function has no business throwing an exception, but we wrap it in a try-catch block just in case.
The example code above is shown with typical catch blocks included, but the key point is that the very first thing we do is register the function entry with the call to enter() and then in our finally block we register the function exit with the call to leave() .
If a function enters more than the number of times allowed by KICKASS_CRYPTO_RECURSION_LIMIT without leaving then an exception is thrown in order to break the recursion. At the time of writing KICKASS_CRYPTO_RECURSION_LIMIT is defined as 100, which is less than the Xdebug limit of 256, which means we should always be able to break our own recursive loops.
And for all the trouble we've gone to if the inheritor calls themselves and recurs directly there is nothing to be done:
class EpicFail extends KickassCryptoOpenSslKickassOpenSslRoundTrip {
protected function do_encrypt( $input ) {
return $this->do_encrypt( $input );
}
}
As mentioned above and elaborated on in the following section this library won't usually throw exceptions from the methods on its public interface because we don't want to leak secrets from our call stack if there's a problem.
Instead of throwing exceptions the methods on the classes in this library will usually return false instead, or some other invalid value such as null or [] .
The avoidance of exceptions is only a firm rule for sensitive function calls which handle secret keys, passphrases, unencrypted content, or any other sensitive data. At the time of writing it's possible for the public get_error_list() function to throw an exception if the implementer has returned an invalid value from do_get_error_list() , apart from in that specific and hopefully unlikely situation everything else should be exception safe and use the boolean value false (or another appropriate sentinel value) to communicate errors to the caller.
Sometimes because of the nature of a typed interface it's not possible to return the boolean value false and in some circumstances the empty string ( '' ), an empty array ( [] ), null ( null ), the floating-point value zero ( 0.0 ), or the integer zero ( 0 ) or minus one ( -1 ) may be returned instead; however, returning false is definitely preferred if it's possible.
Aside: in some cases minus one ( -1 ) can be used as the sentinel value to signal an error, such as when you want to indicate an invalid array index or an invalid count, but unlike in some other languages in PHP minus one isn't necessarily an invalid array index, and returning false is still preferred. This library does use minus one in some cases, if there's a problem with managing the telemetry counters.
The fact that an error has occurred can be registered with your component by a call to error() so that if the callers get a false return value they can interrogate your component with a call to get_error() or get_error_list() to get the recent errors (the caller can clear these errors with clear_error() too).
In our library the function for registering that an error has occurred is the error() function defined in the KickassCrypto class.
In some error situations the best and safest thing to do is swallow the error and return a sensible and safe and uncontroversial default value as a fallback.
Here's a quick run-down:
get_error_list() you get an exception with no errorget_error() you get null and an errorclear_error() it's void but with an errorhandle() you get a log entry, no errornotify() it will be handled then ignored, no errorignore() you get a log entry, no errorthrow() it will throw anywayerror() your error may not be properly registered, it always returns falsecount_*() counter you get -1 and no errorincrement_counter() you get -1 and no errorget_const_data_format() you get an empty string and no errorget_const_*() constant accessor you get the value defined by the default constant and no errorget_config_*() config accessor you get the value defined by the default constant (or false if there is no such thing) and no errorget_const() you get the default value and no errorget_passphrase_list() you get an empty array and an errorget_encryption_passphrase() you get null and no erroris_*() method you will get false and no errorget_data_encoding() you will get an empty string and no errorget_data_format() you will get false and no errorconvert_secret_to_passphrase() you will get false and no errorget_padding() you will get false and no errorget_delay() you will get false and no error (an emergency delay will be injected)delay() you will get void and no error (an emergency delay will be injected)log_error() you will get false and no error (but we try to be forgiving)This library is very particular about exception handling and error reporting.
If you have sensitive data on your call stack you must not throw exceptions. Sensitive data includes:
If you encounter a situation from which you cannot continue processing of the typical and expected program logic the way to register this problem is by calling the error() function with a string identifying and describing the problem and then returning false to indicate failure.
As the error() function always returns the boolean value false you can usually register the error and return false on the same like, like this:
return $this->error( __FUNCTION__, 'something bad happened.' );
When I nominate error strings I usually start them with a lowercase letter and end them with a period.
Note that it's okay to intercept and rethrow PHP AssertionError exceptions. These should only ever occur during development and not in production. If you're calling code you don't trust you might not wish to rethrow AssertionError exceptions, but if you're calling code you don't trust you've probably got bigger problems in life.
If you have a strong opinion regarding AssertionError exceptions and think I should not rethrow them I would be happy to hear from you to understand your concern and potentially address the issue.
Following is some example code showing how to handle exceptions and manage errors.
protected final function do_work_with_secret( $secret ) {
try {
$result = str_repeat( $secret, 2 );
$this->call_some_function_you_might_not_control( $result );
return $result;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
}
try {
return $this->error( __FUNCTION__, 'error working with string.' );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
return false;
}
In actual code you would define an error constant for use instead of the string literal 'error working with string.' . In this library the names of error constants begin with "KICKASS_CRYPTO_ERROR_" and they are defined in the src/code/global/constant/framework.php file.
Note that we don't even assume it's safe to call handle() , ignore() , or error() ; we wrap all such calls in try-catch handlers too. There are some edge case situations where even these functions which are supposed to be thread safe can lead to exceptions, such as when there's infinite recursion which gets aborted by the run-time. If you're an expert on such matters the code might do with a review from you.
Now I will agree that the above code is kind of insane, it's just that it seems to me like there's no avoiding it if we want to be safe. We have to explicitly allow the AssertionError exception every single time in every single method just so that assertions remain useful to us as a development tool, and then when we handle other exceptions we want to make some noise about them so we call handle() , but the thing is that handle() will defer to do_handle() which can be overridden by implementers, which means it can throw... so if handle() throws we don't want to just do nothing, we want to give the programmer a last chance to learn of their errant code, so we notify that we're going to ignore the exception with a call to ignore() , but that will defer to do_ignore() , which the programmer could override, and throw from... but if that happens we will just silently ignore such a problem.
And then if we get through all of that and our function hasn't returned then that's an error situation so we want to notify the error, but error() defers to do_error() and that could be overridden and throw, so we wrap in a try-catch block and then do the exception ignore dance again.
I mean it's all over the top and excessive but it should at least be safe and it meets two requirements:
In the usual happy code path none of the exception handling code even runs.
There are a bunch of functions for testing boolean conditions, and they begin with "is_" and return a boolean. These functions should only do the test and return true or false, they should not register errors using the error() function, if that's necessary the caller will do that.
The is_() functions can be implemented using the typed final wrapper idiom documented above.
Following is a good example from the code.
protected final function is_valid_secret( $secret ) : bool {
try {
$is_valid = $this->do_is_valid_secret( $secret );
// ...
assert( is_bool( $is_valid ) );
return $is_valid;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
}
return false;
}
Note that do_is_valid_secret() also has a secret on the call stack, so it should be implemented as exception safe in the same way (in case it is called directly from some other part of the code).
Note too that it's okay to just rethrow assertion violations, these should never happen in production and they make testing the code easier.
The approach to unit-testing taken by this library is simple and powerful. There are three types of test which can be defined for each unit test:
Each script will be either a shell script with the same name, eg fast.sh , or if that's missing a PHP script with the same name, eg fast.php . The test runner just finds these scripts and runs them. This is easy to do and provides all the power we need to run our tests, including support for the various situations where each test instance needs to run in its own process and be isolated from other testing environments.
If you have flakey and unreliable tests you can stick them in as silly tests. The fast and slow tests are the important ones, and you shouldn't put slow tests in the fast test scripts. The fast tests are for day to day programming and testing and the slow scripts are for running prior to a version release.
Here are some notes regarding notable components:
config.php file for the demoSome countries have banned the import or use of strong cryptography, such as 256 bit AES.
Please be advised that this library does not contain cryptographic functions, they are provided by your PHP implementation.
Copyright (c) 2023 John Elliot V.
This code is licensed under the MIT License.
See the contributors file.
I should probably be more disciplined with my commit messages... if this library matures and gets widely used I will try to be more careful with my commits.
The Kickass Crypto ASCII banner is in the Graffiti font courtesy of TAAG.
The string "kickass" appears in the source code 1,506 times (including the ASCII banners).
SLOC and file count reports generated using David A. Wheeler's 'SLOCCount'.
I'd love to hear from you! Hit me up at [email protected]. Put "Kickass Crypto" in the subject line to make it past my mail filters.