Merge backend into main, conservando archivos de backend

This commit is contained in:
christian.julian 2025-06-20 11:29:29 -06:00
commit b8aaa87509
407 changed files with 49868 additions and 0 deletions
api_candidatos
api
composer.jsoncomposer.lockindex.htmlopenapiprivate.keypublic.key
scripts
src
vendor

View File

@ -0,0 +1,58 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../src/Utils/CorsHandler.php';
// Configurar CORS para todas las respuestas
\Utils\CorsHandler::configureCors();
header('Content-Type: application/json; charset=utf-8');
// Loader para Slim PSR-7
require_once __DIR__ . '/../vendor/autoload.php';
// Obtener y normalizar ruta
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// Quitar el prefijo '/api' para trabajar con rutas limpias
$basePath = '/api_candidatos';
$normalizedPath = str_replace($basePath, '', $path);
// Enrutamiento
switch (true) {
case preg_match('#^/oauth/token#', $normalizedPath):
$authPath = __DIR__ . '/../src/OAuth/Routes/AuthRoutes.php';
if (file_exists($authPath)) {
require_once $authPath;
} else {
http_response_code(500);
echo json_encode([
'error' => 'Archivo AuthRoutes.php no encontrado',
'ruta_buscada' => $authPath
]);
exit;
}
// /../src/candidatos/index.php
break;
case preg_match('#^/candidatos#', $normalizedPath):
// Verificar token de OAuth 2.0 antes de acceder a rutas protegidas
require_once __DIR__ . '/../src/OAuth/Middleware/AuthMiddleware.php';
// Solo continuamos si el token es válido
if (\OAuth\Middleware\AuthMiddleware::verifyToken()) {
require_once __DIR__ . '/../src/candidatos/index.php';
}
break;
default:
http_response_code(404);
echo json_encode(["error" => "Ruta no encontrada"]);
break;
}
?>

View File

@ -0,0 +1,15 @@
{
"require": {
"league/oauth2-server": "^8.4",
"defuse/php-encryption": "^2.4",
"slim/psr7": "^1.7"
},
"autoload": {
"psr-4": {
"OAuth\\": "src/OAuth/",
"Config\\": "src/Config/"
}
}
}

1054
api_candidatos/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

536
api_candidatos/index.html Normal file
View File

@ -0,0 +1,536 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sistema de Candidatos</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
.token-info {
word-break: break-all;
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
pre {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
}
.hidden {
display: none;
}
.success-message {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
}
.download-section {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border: 1px solid #dee2e6;
}
.download-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.download-btn {
min-width: 120px;
}
</style>
</head>
<body>
<div class="container mt-5">
<h1 class="mb-4">Sistema de Candidatos - OAuth 2.0 Test</h1>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<h5>Credenciales del Cliente</h5>
</div>
<div class="card-body">
<form id="oauth-form">
<div class="mb-3">
<label for="clientId" class="form-label">Client ID</label>
<input type="text" class="form-control" id="clientId" required />
</div>
<div class="mb-3">
<label for="clientSecret" class="form-label">Client Secret</label>
<input type="password" class="form-control" id="clientSecret" required />
</div>
<button type="submit" class="btn btn-primary">Obtener Token</button>
</form>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card" id="token-card">
<div class="card-header">
<h5>Estado de Autenticación</h5>
</div>
<div class="card-body">
<div id="token-section" class="hidden">
<div class="success-message">
<p><strong>✓ Autenticación exitosa</strong></p>
<p><strong>Token tipo:</strong> <span id="token-type">-</span></p>
<p><strong>Token válido por:</strong> <span id="expires-in">-</span> segundos</p>
<p><small class="text-muted">El token de acceso se ha guardado de forma segura</small></p>
</div>
<button id="get-candidates" class="btn btn-success">Obtener Candidatos</button>
</div>
<div id="no-token-message">
<p class="text-muted">Ingrese sus credenciales y haga clic en "Obtener Token" para comenzar.</p>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card" id="result-card">
<div class="card-header">
<h5>Resultados</h5>
</div>
<div class="card-body">
<div id="download-section" class="download-section hidden">
<h6 class="mb-3">📥 Descargar Datos</h6>
<div class="download-buttons">
<button id="download-json" class="btn btn-outline-primary download-btn">
📄 JSON
</button>
<button id="download-csv" class="btn btn-outline-success download-btn">
📊 CSV
</button>
<button id="download-excel" class="btn btn-outline-info download-btn">
📈 Excel
</button>
</div>
<small class="text-muted d-block mt-2">
Total de registros: <span id="records-count">0</span>
</small>
</div>
<div id="candidates-section" class="hidden">
<h4 class="mb-3">Lista de Candidatos</h4>
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Correo</th>
<th>Teléfono</th>
<th>Género</th>
<th>Rango Edad</th>
<th>Tipo Identificación</th>
<th>País</th>
<th>Estado</th>
<th>Municipio</th>
<th>Colonia</th>
<th>Nivel Estudio</th>
<th>Giro</th>
<th>Empresa/Institución</th>
<th>ID Examen</th>
<th>Nombre Examen</th>
<th>Motivo</th>
<th>Calificación</th>
<th>Consentimiento Publicidad</th>
<th>Fecha Entrada</th>
<th>Fecha Salida</th>
</tr>
</thead>
<tbody id="candidates-table-body"></tbody>
</table>
</div>
</div>
<div id="api-response-section" class="hidden">
<h4 class="mb-3">Respuesta JSON</h4>
<pre id="api-response">Esperando respuesta...</pre>
</div>
<div id="error-section" class="hidden">
<div class="alert alert-danger" id="error-message"></div>
</div>
<div id="no-results-message">
<p class="text-muted">Obtenga un token y haga una solicitud para ver los resultados aquí.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// ==========================================
// CONFIGURACIÓN Y CONSTANTES
// ==========================================
const CONFIG = {
API_BASE_URL: 'http://localhost:5000/api',
FILE_PREFIX: 'candidatos'
};
// ==========================================
// ESTADO DE LA APLICACIÓN
// ==========================================
class AppState {
constructor() {
this.accessToken = '';
this.candidatesData = [];
}
setAccessToken(token) {
this.accessToken = token;
}
getAccessToken() {
return this.accessToken;
}
setCandidatesData(data) {
this.candidatesData = data;
}
getCandidatesData() {
return this.candidatesData;
}
hasToken() {
return !!this.accessToken;
}
hasData() {
return this.candidatesData.length > 0;
}
}
// ==========================================
// SERVICIOS API
// ==========================================
class ApiService {
static async getAccessToken(clientId, clientSecret) {
const formData = new URLSearchParams();
formData.append('grant_type', 'client_credentials');
formData.append('client_id', clientId);
formData.append('client_secret', clientSecret);
const response = await fetch(`${CONFIG.API_BASE_URL}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Error en la autenticación');
}
return await response.json();
}
static async getCandidates(accessToken) {
if (!accessToken) {
throw new Error('No hay token de acceso disponible');
}
const response = await fetch(`${CONFIG.API_BASE_URL}/candidatos/obtenerCandidatos`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Error al obtener candidatos');
}
return await response.json();
}
}
// ==========================================
// UTILIDADES
// ==========================================
class Utils {
static getCurrentDateTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}${month}${day}_${hours}${minutes}`;
}
static flattenObject(obj, prefix = '') {
const flattened = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = prefix ? `${prefix}_${key}` : key;
if (obj[key] !== null && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
Object.assign(flattened, Utils.flattenObject(obj[key], newKey));
} else {
flattened[newKey] = obj[key];
}
}
}
return flattened;
}
static convertToCSV(data) {
if (!data.length) return '';
const flatData = data.map(candidato => Utils.flattenObject(candidato));
const headers = Object.keys(flatData[0]);
const csvHeaders = headers.join(',');
const csvRows = flatData.map(row =>
headers.map(header => {
const value = row[header];
if (typeof value === 'string' && (value.includes(',') || value.includes('\n') || value.includes('"'))) {
return `"${value.replace(/"/g, '""')}"`;
}
return value || '';
}).join(',')
);
return [csvHeaders, ...csvRows].join('\n');
}
static downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
}
// ==========================================
// CONTROLADOR DE DESCARGA
// ==========================================
class DownloadController {
constructor(appState) {
this.appState = appState;
}
downloadJSON() {
if (!this.appState.hasData()) {
alert('No hay datos para descargar');
return;
}
const dataStr = JSON.stringify(this.appState.getCandidatesData(), null, 2);
const filename = `${CONFIG.FILE_PREFIX}_${Utils.getCurrentDateTime()}.json`;
Utils.downloadFile(dataStr, filename, 'application/json');
}
downloadCSV() {
if (!this.appState.hasData()) {
alert('No hay datos para descargar');
return;
}
const csvData = Utils.convertToCSV(this.appState.getCandidatesData());
const filename = `${CONFIG.FILE_PREFIX}_${Utils.getCurrentDateTime()}.csv`;
Utils.downloadFile(csvData, filename, 'text/csv;charset=utf-8;');
}
downloadExcel() {
if (!this.appState.hasData()) {
alert('No hay datos para descargar');
return;
}
const flatData = this.appState.getCandidatesData().map(candidato => Utils.flattenObject(candidato));
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(flatData);
const cols = Object.keys(flatData[0]).map(() => ({ wch: 15 }));
ws['!cols'] = cols;
XLSX.utils.book_append_sheet(wb, ws, 'Candidatos');
const filename = `${CONFIG.FILE_PREFIX}_${Utils.getCurrentDateTime()}.xlsx`;
XLSX.writeFile(wb, filename);
}
}
// ==========================================
// CONTROLADOR DE UI
// ==========================================
class UIController {
constructor() {
this.elements = {
tokenType: document.getElementById('token-type'),
expiresIn: document.getElementById('expires-in'),
tokenSection: document.getElementById('token-section'),
noTokenMessage: document.getElementById('no-token-message'),
downloadSection: document.getElementById('download-section'),
recordsCount: document.getElementById('records-count'),
candidatesSection: document.getElementById('candidates-section'),
candidatesTableBody: document.getElementById('candidates-table-body'),
apiResponseSection: document.getElementById('api-response-section'),
apiResponse: document.getElementById('api-response'),
errorSection: document.getElementById('error-section'),
errorMessage: document.getElementById('error-message'),
noResultsMessage: document.getElementById('no-results-message')
};
}
displayTokenInfo(tokenData) {
this.elements.tokenType.textContent = tokenData.token_type;
this.elements.expiresIn.textContent = tokenData.expires_in;
this.elements.tokenSection.classList.remove('hidden');
this.elements.noTokenMessage.classList.add('hidden');
console.log('Token obtenido y almacenado de forma segura');
}
displayCandidates(candidates) {
this.elements.apiResponse.textContent = JSON.stringify(candidates, null, 2);
this.elements.apiResponseSection.classList.remove('hidden');
this.elements.downloadSection.classList.remove('hidden');
this.elements.recordsCount.textContent = candidates.length;
this._fillCandidatesTable(candidates);
this.elements.candidatesSection.classList.remove('hidden');
this.elements.noResultsMessage.classList.add('hidden');
}
_fillCandidatesTable(candidates) {
this.elements.candidatesTableBody.innerHTML = '';
candidates.forEach(candidato => {
const dem = candidato.demograficos || {};
const ubi = dem.ubicacion || {};
const form = candidato.formacion || {};
const exam = candidato.examen || {};
const exp = candidato.experiencia_servicio || {};
const fechas = candidato.fechas || {};
const row = document.createElement('tr');
row.innerHTML = `
<td>${candidato.id_candidato ?? ''}</td>
<td>${candidato.nombre_completo ?? ''}</td>
<td>${candidato.contacto?.correo ?? ''}</td>
<td>${candidato.contacto?.telefono ?? ''}</td>
<td>${dem.genero ?? ''}</td>
<td>${dem.rango_edad ?? ''}</td>
<td>${dem.tipo_identificacion ?? ''}</td>
<td>${ubi.pais ?? ''}</td>
<td>${ubi.estado ?? ''}</td>
<td>${ubi.municipio ?? ''}</td>
<td>${ubi.colonia ?? ''}</td>
<td>${form.nivel_estudio ?? ''}</td>
<td>${form.giro ?? ''}</td>
<td>${form.nombre_empresa_institucion ?? ''}</td>
<td>${exam.id_examen ?? ''}</td>
<td>${exam.nombre_examen ?? ''}</td>
<td>${exam.motivo ?? ''}</td>
<td>${exp.calificacion_servicio ?? ''}</td>
<td>${exp.consentimiento_publicidad !== undefined ? (exp.consentimiento_publicidad ? 'Sí' : 'No') : ''}</td>
<td>${fechas.entrada ?? ''}</td>
<td>${fechas.salida ?? ''}</td>
`;
this.elements.candidatesTableBody.appendChild(row);
});
}
showError(message) {
this.elements.errorMessage.textContent = message;
this.elements.errorSection.classList.remove('hidden');
this.elements.noResultsMessage.classList.add('hidden');
}
hideAllMessages() {
this.elements.errorSection.classList.add('hidden');
this.elements.apiResponseSection.classList.add('hidden');
this.elements.downloadSection.classList.add('hidden');
}
}
// ==========================================
// CONTROLADOR PRINCIPAL DE LA APLICACIÓN
// ==========================================
class AppController {
constructor() {
this.appState = new AppState();
this.uiController = new UIController();
this.downloadController = new DownloadController(this.appState);
this._initializeEventListeners();
}
_initializeEventListeners() {
// OAuth form
document.getElementById('oauth-form').addEventListener('submit', (e) => this._handleOAuthSubmit(e));
// Get candidates button
document.getElementById('get-candidates').addEventListener('click', () => this._handleGetCandidates());
// Download buttons
document.getElementById('download-json').addEventListener('click', () => this.downloadController.downloadJSON());
document.getElementById('download-csv').addEventListener('click', () => this.downloadController.downloadCSV());
document.getElementById('download-excel').addEventListener('click', () => this.downloadController.downloadExcel());
}
async _handleOAuthSubmit(e) {
e.preventDefault();
const clientId = document.getElementById('clientId').value;
const clientSecret = document.getElementById('clientSecret').value;
this.uiController.hideAllMessages();
try {
const response = await ApiService.getAccessToken(clientId, clientSecret);
this.appState.setAccessToken(response.access_token);
this.uiController.displayTokenInfo(response);
} catch (error) {
this.uiController.showError(`Error al obtener token: ${error.message}`);
}
}
async _handleGetCandidates() {
this.uiController.hideAllMessages();
try {
const candidates = await ApiService.getCandidates(this.appState.getAccessToken());
this.appState.setCandidatesData(candidates);
this.uiController.displayCandidates(candidates);
} catch (error) {
this.uiController.showError(`Error al obtener candidatos: ${error.message}`);
}
}
}
// ==========================================
// INICIALIZACIÓN DE LA APLICACIÓN
// ==========================================
document.addEventListener('DOMContentLoaded', function() {
new AppController();
});
</script>
</body>
</html>

218
api_candidatos/openapi Normal file
View File

@ -0,0 +1,218 @@
openapi: 3.0.3
info:
title: API de Candidatos
description: |
API para gestionar información de candidatos con autenticación OAuth 2.0.
Permite obtener información detallada de candidatos incluyendo datos demográficos,
formación académica, exámenes y experiencia de servicio.
version: 1.0.0
servers:
- url: /api
description: Servidor principal
security:
- oauth2: []
paths:
/oauth/token:
post:
summary: Obtener token de acceso OAuth 2.0
description: Endpoint para obtener un token de acceso utilizando credenciales de cliente
tags:
- Autenticación
security: []
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required:
- grant_type
- client_id
- client_secret
properties:
grant_type:
type: string
enum: [client_credentials]
description: Tipo de autorización OAuth 2.0
client_id:
type: string
description: Identificador del cliente
client_secret:
type: string
description: Secreto del cliente
scope:
type: string
description: Alcances solicitados (opcional)
responses:
'200':
description: Token de acceso generado exitosamente
content:
application/json:
schema:
type: object
properties:
token_type:
type: string
example: Bearer
expires_in:
type: integer
example: 3600
access_token:
type: string
example: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
'400':
description: Solicitud inválida o credenciales incorrectas
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: No autorizado
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/candidatos/obtenerCandidatos:
get:
summary: Obtener lista de candidatos
description: Devuelve un listado de todos los candidatos con información detallada
tags:
- Candidatos
responses:
'200':
description: Lista de candidatos obtenida correctamente
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Candidato'
'401':
description: No autorizado - Token de acceso inválido o expirado
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Error del servidor
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
securitySchemes:
oauth2:
type: oauth2
flows:
clientCredentials:
tokenUrl: /api/oauth/token
scopes: {}
schemas:
Error:
type: object
properties:
error:
type: string
example: invalid_request
message:
type: string
example: El token de acceso proporcionado es inválido
Candidato:
type: object
properties:
id_candidato:
type: string
description: Identificador único del candidato
nombre_completo:
type: string
description: Nombre completo del candidato
contacto:
type: object
properties:
correo:
type: string
format: email
description: Correo electrónico del candidato
telefono:
type: string
description: Número telefónico del candidato
demograficos:
type: object
properties:
genero:
type: string
description: Género del candidato
rango_edad:
type: string
description: Rango de edad del candidato
tipo_identificacion:
type: string
description: Tipo de identificación del candidato
ubicacion:
type: object
properties:
pais:
type: string
description: País de residencia
estado:
type: string
description: Estado o provincia
municipio:
type: string
description: Municipio o ciudad
colonia:
type: string
description: Colonia o barrio
formacion:
type: object
properties:
nivel_estudio:
type: string
description: Nivel de estudios del candidato
giro:
type: string
description: Sector o giro de actividad
nombre_empresa_institucion:
type: string
description: Nombre de la empresa o institución donde trabaja/estudia
examen:
type: object
properties:
id_examen:
type: string
description: Identificador del examen
nombre_examen:
type: string
description: Nombre del examen realizado
motivo:
type: string
description: Motivo por el cual realizó el examen
experiencia_servicio:
type: object
properties:
calificacion_servicio:
type: integer
minimum: 0
maximum: 10
description: Calificación del servicio (0-10)
consentimiento_publicidad:
type: boolean
description: Consentimiento para recibir publicidad
fechas:
type: object
properties:
entrada:
type: string
format: date-time
description: Fecha y hora de entrada
salida:
type: string
format: date-time
description: Fecha y hora de salida

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDmIlzzZREHBOsT
UYagVBBLo3OWOmR4xf/JTdE6LKte03MUL1340RCxqKnMHNddjgdfCs5nLPXq8cJz
Bhmg/HvdNZbzxRAvdX3GRFEg5Clh6Nx0xLKqs+Luon8E1qr6a/d8y3TfGJ7CMsid
wNNjTV2QI16R/ntPPcpy+mFBPXhzDNX4rJfvKq+dz7Py/SwOimLFhHAGhBbFv8v+
1Yd2ZnFvQGUDZNEfmRJlyNznPW6aEa7vu7XuIuC7tTNCQ/cMGyQUgaJxWev3iH8r
srH4I/TYyKuRUjv14Km3XgEvo5DwA8f0ZeXrgv0iSHe8+qbvf92DU2KBwZVS5Z3J
GfP8sdazAgMBAAECggEAJwZ1v7qISCnv2S9OhpqqxsDZhYSya/6bkR32mIhhqStv
TOF5bIu7an0hCiFr7gv7OQVCmiF4NFa59Dp5FyEpugnv5reotnuUkA4eudanI9jS
paSDbcoidfgtVPs4NE4hwlJYJ8rrhSAKgCHmVuUUNDCjRVujunzOe2/1FRSg+9VD
ivUjDa8GQ0Ydzt0wKM1GPnhUDJms+kM+mopRVTSqijFE3nr+JNX9ZiMIp4T3KpwR
WVIjilFAU6658MHn8EGcIdTGU0CLcBSvZ6PdK7uMbjdgvX666AHJ7gjEFCj56lCF
hwamExFMf1smHVg2HrC5hKj4o53CqNlbbIKS1GJh7QKBgQD5uBUBxYek2x/7OENf
TsMX2ntseiM31cgnYFSE4bwodRqZ8A/+sjQKCWuTDOFoAj8oRsrkY9DlaL10Gr/i
u4eXKU1/mVxXpefjiMoWx7ly0eeyRuvDx6TkIO/NdnY0ND5umSVAPx8VVOAUIOdZ
w9e0E9URSj2qy3IfAoaPZgz6vQKBgQDr7C0Sf5oVJ2jxzJbsmGbPLgIIpvqMILdG
UZGWM0XkhkkmkKI3hUR0qDtc2BlqZvWqvEGAgQfIU3Y3wTWVOgYhHbi+ylVxMvQQ
c3waneWF2DbWgri9zG8GuSmug2x0GIjT/nOn7QSbpRrF71bu4+3oPMskzaMTgYTt
LJkZCNLmLwKBgDQgKMJl6RqQYuydofKTDkY8ZOcP16ogBdeyU/Io7I3FY/geFDim
Gha+QKZBWgvL7EMMA+4Ip+I7KtDBhKxfWL5E8NhhutTQ3MayFv0KU7uT9TlRdIU5
d0HnXicVQzdCcIXFkfEHPAXH4b5R3/js2GnOeftR8+1i6j9u14e3VZ5BAoGBAKoi
sgZrGxUyTI5Dunt5FHtIdJMEyB6R4VnGrTUiWL8K0GoNV86uPsXaJKU5+movQe8U
wDAJ3TDsb46ZuSiapZzwMDD2/VMbKcNLZS5UvBcf67wanVvSuCajFZoSkP3QS6yG
DaYGWZJdKMehaJHysbkPTniGC5qfhtr7lJTnNiBlAoGBAMeYwvc1e0BBktP0RsYg
jwS8KgREYdM7nJf2hglJkpxJ5sg+7bmbyRLHFi8xz3VfNHNFOq9U6RFglhEUyEik
xMnpk8a2CDkbQXuGhUYi/NDGCIetwsiUzMxxYwrCj0KNRbHwzr9lFOlykxsDdJjc
xH9xnca2J8M3Rdp15gTMH7JG
-----END PRIVATE KEY-----

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5iJc82URBwTrE1GGoFQQ
S6NzljpkeMX/yU3ROiyrXtNzFC9d+NEQsaipzBzXXY4HXwrOZyz16vHCcwYZoPx7
3TWW88UQL3V9xkRRIOQpYejcdMSyqrPi7qJ/BNaq+mv3fMt03xiewjLIncDTY01d
kCNekf57Tz3KcvphQT14cwzV+KyX7yqvnc+z8v0sDopixYRwBoQWxb/L/tWHdmZx
b0BlA2TRH5kSZcjc5z1umhGu77u17iLgu7UzQkP3DBskFIGicVnr94h/K7Kx+CP0
2MirkVI79eCpt14BL6OQ8APH9GXl64L9Ikh3vPqm73/dg1NigcGVUuWdyRnz/LHW
swIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,32 @@
<?php
/**
* Script para generar las claves pública y privada necesarias para OAuth 2.0
*/
// Directorio de la aplicación
$appDir = dirname(__DIR__);
echo "Generando clave privada...\n";
// Generar clave privada RSA
$privateKey = openssl_pkey_new([
'private_key_bits' => 2048, // Tamaño de la clave
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
// Guardar la clave privada en un archivo
openssl_pkey_export_to_file($privateKey, $appDir . '/private.key');
echo "Clave privada generada en: " . $appDir . "/private.key\n";
// Obtener los detalles de la clave privada para generar la clave pública
$keyDetails = openssl_pkey_get_details($privateKey);
// Guardar la clave pública en un archivo
file_put_contents($appDir . '/public.key', $keyDetails['key']);
echo "Clave pública generada en: " . $appDir . "/public.key\n";
echo "¡Listo! Las claves se han generado correctamente.\n";
?>

View File

@ -0,0 +1,24 @@
<?php
namespace Config;
use mysqli;
// Database connection class
class Database {
public static function connect() {
$host = "localhost";
$user = "laniacc";
$password = "<U]93Cl3";
$database = "laniacc";
$conn = new mysqli($host, $user, $password, $database);
if ($conn->connect_error) {
die("Error de conexión a la base de datos: " . $conn->connect_error);
}
$conn->set_charset("utf8mb4");
return $conn;
}
}
?>

View File

@ -0,0 +1,10 @@
<?php
return [
'private_key_path' => __DIR__ . '/../../private.key',
'public_key_path' => __DIR__ . '/../../public.key',
'encryption_key' => 'lkj7LKJ98y0897YJHjh6&%^',
'access_token_ttl' => 3600, // 1 hora
'refresh_token_ttl' => 86400 * 30, // 30 días
];
?>

View File

@ -0,0 +1,66 @@
<?php
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
use OAuth\Repositories\AccessTokenRepository;
use OAuth\Repositories\ClientRepository;
use OAuth\Repositories\ScopeRepository;
use OAuth\Repositories\RefreshTokenRepository;
use Config\Database;
class AuthController {
public static function token() {
header('Content-Type: application/json; charset=UTF-8');
$config = require_once __DIR__ . '/../../Config/oauth.php';
$conn = Database::connect();
// Configurar server
$clientRepository = new ClientRepository();
$scopeRepository = new ScopeRepository();
$accessTokenRepository = new AccessTokenRepository($conn);
// Crear authorization server
$server = new AuthorizationServer(
$clientRepository,
$accessTokenRepository,
$scopeRepository,
$config['private_key_path'],
$config['encryption_key']
);
// Habilitar Client Credentials Grant
$server->enableGrantType(
new ClientCredentialsGrant(),
new \DateInterval('PT' . $config['access_token_ttl'] . 'S')
);
// Procesar petición
try {
$response = $server->respondToAccessTokenRequest(
\Slim\Psr7\Factory\ServerRequestFactory::createFromGlobals(),
new \Slim\Psr7\Response()
);
// Enviar respuesta
echo $response->getBody();
} catch (OAuthServerException $e) {
// Errores de OAuth
http_response_code($e->getHttpStatusCode());
echo json_encode([
'error' => $e->getErrorType(),
'message' => $e->getMessage()
]);
} catch (\Exception $e) {
// Errores del servidor
http_response_code(500);
echo json_encode([
'error' => 'server_error',
'message' => $e->getMessage(), // MOSTRAR EL MENSAJE REAL
'trace' => $e->getTraceAsString() // Opcional: útil para depurar
]);
}
}
}
?>

View File

@ -0,0 +1,13 @@
<?php
namespace OAuth\Entities;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
class AccessTokenEntity implements AccessTokenEntityInterface
{
use AccessTokenTrait, TokenEntityTrait, EntityTrait;
}
?>

View File

@ -0,0 +1,37 @@
<?php
namespace OAuth\Entities;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\Traits\ClientTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
class ClientEntity implements ClientEntityInterface
{
use EntityTrait, ClientTrait;
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function setRedirectUri($uri)
{
$this->redirectUri = $uri;
}
public function getRedirectUri()
{
return $this->redirectUri;
}
public function isConfidential()
{
return true;
}
}
?>

View File

@ -0,0 +1,12 @@
<?php
namespace OAuth\Entities;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait;
class RefreshTokenEntity implements RefreshTokenEntityInterface
{
use RefreshTokenTrait, EntityTrait;
}
?>

View File

@ -0,0 +1,16 @@
<?php
namespace OAuth\Entities;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
class ScopeEntity implements ScopeEntityInterface
{
use EntityTrait;
public function jsonSerialize(): mixed
{
return $this->getIdentifier();
}
}
?>

View File

@ -0,0 +1,51 @@
<?php
namespace OAuth\Middleware;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use OAuth\Repositories\AccessTokenRepository;
use Slim\Psr7\Factory\ServerRequestFactory;
use Config\Database;
class AuthMiddleware {
public static function verifyToken() {
$config = require_once __DIR__ . '/../../Config/oauth.php';
$conn = Database::connect();
$accessTokenRepository = new AccessTokenRepository($conn);
$resourceServer = new ResourceServer(
$accessTokenRepository,
$config['public_key_path']
);
try {
$request = \Slim\Psr7\Factory\ServerRequestFactory::createFromGlobals();
$request = $resourceServer->validateAuthenticatedRequest($request);
return true;
} catch (OAuthServerException $e) {
// Token no válido
header('Content-Type: application/json; charset=UTF-8');
http_response_code($e->getHttpStatusCode());
echo json_encode([
'error' => $e->getErrorType(),
'message' => $e->getMessage()
]);
exit;
} catch (\Exception $e) {
// Error del servidor
header('Content-Type: application/json; charset=UTF-8');
http_response_code(500);
echo json_encode([
'error' => 'server_error',
'message' => 'Se produjo un error en el servidor'
]);
exit;
}
}
}
?>

View File

@ -0,0 +1,77 @@
<?php
namespace OAuth\Repositories;
require_once __DIR__ . '/../../Config/Database.php';
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use mysqli;
class AccessTokenRepository implements AccessTokenRepositoryInterface
{
private $conn;
public function __construct(mysqli $conn)
{
$this->conn = $conn;
}
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity)
{
$stmt = $this->conn->prepare(
'INSERT INTO oauth_access_tokens (access_token, client_id, user_id, expires, scope) VALUES (?, ?, ?, ?, ?)'
);
$token = $accessTokenEntity->getIdentifier();
$clientId = $accessTokenEntity->getClient()->getIdentifier();
$userId = $accessTokenEntity->getUserIdentifier();
$expires = date('Y-m-d H:i:s', $accessTokenEntity->getExpiryDateTime()->getTimestamp());
$scopes = $this->scopesToString($accessTokenEntity->getScopes());
$stmt->bind_param('sssss', $token, $clientId, $userId, $expires, $scopes);
$stmt->execute();
}
public function revokeAccessToken($tokenId)
{
$stmt = $this->conn->prepare('DELETE FROM oauth_access_tokens WHERE access_token = ?');
$stmt->bind_param('s', $tokenId);
$stmt->execute();
}
public function isAccessTokenRevoked($tokenId)
{
$stmt = $this->conn->prepare('SELECT 1 FROM oauth_access_tokens WHERE access_token = ?');
$stmt->bind_param('s', $tokenId);
$stmt->execute();
$result = $stmt->get_result();
return $result->num_rows === 0;
}
public function getNewToken($client, array $scopes, $userIdentifier = null)
{
$accessToken = new \OAuth\Entities\AccessTokenEntity();
$accessToken->setClient($client);
if ($userIdentifier !== null) {
$accessToken->setUserIdentifier($userIdentifier);
}
foreach ($scopes as $scope) {
$accessToken->addScope($scope);
}
return $accessToken;
}
private function scopesToString(array $scopes)
{
return implode(' ', array_map(function ($scope) {
return $scope->getIdentifier();
}, $scopes));
}
}
?>

View File

@ -0,0 +1,63 @@
<?php
namespace OAuth\Repositories;
use Config\Database;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
class ClientRepository implements ClientRepositoryInterface
{
protected $conn;
public function __construct()
{
$this->conn = Database::connect();
}
public function getClientEntity($clientIdentifier)
{
$stmt = $this->conn->prepare('SELECT * FROM oauth_clients WHERE client_id = ?');
$stmt->bind_param('s', $clientIdentifier);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
return null;
}
$client = $result->fetch_assoc();
$clientEntity = new \OAuth\Entities\ClientEntity();
$clientEntity->setIdentifier($client['client_id']);
$clientEntity->setName($client['client_id']);
return $clientEntity;
}
public function validateClient($clientIdentifier, $clientSecret, $grantType)
{
$stmt = $this->conn->prepare('SELECT * FROM oauth_clients WHERE client_id = ?');
$stmt->bind_param('s', $clientIdentifier);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
return false;
}
$client = $result->fetch_assoc();
if (
$client['client_secret'] === $clientSecret &&
(
$client['grant_types'] === null ||
in_array($grantType, explode(',', $client['grant_types']))
)
) {
return true;
}
return false;
}
}
?>

View File

@ -0,0 +1,55 @@
<?php
namespace OAuth\Repositories;
require_once __DIR__ . '/../../Config/Database.php';
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
class RefreshTokenRepository implements RefreshTokenRepositoryInterface
{
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity)
{
global $conn;
$refreshToken = $refreshTokenEntity->getIdentifier();
$accessToken = $refreshTokenEntity->getAccessToken()->getIdentifier();
$expires = date('Y-m-d H:i:s', $refreshTokenEntity->getExpiryDateTime()->getTimestamp());
$clientId = $refreshTokenEntity->getAccessToken()->getClient()->getIdentifier();
$userId = $refreshTokenEntity->getAccessToken()->getUserIdentifier();
$stmt = $conn->prepare(
'INSERT INTO oauth_refresh_tokens (refresh_token, access_token, client_id, user_id, expires) VALUES (?, ?, ?, ?, ?)'
);
$stmt->bind_param('sssss', $refreshToken, $accessToken, $clientId, $userId, $expires);
$stmt->execute();
}
public function revokeRefreshToken($tokenId)
{
global $conn;
$stmt = $conn->prepare('DELETE FROM oauth_refresh_tokens WHERE refresh_token = ?');
$stmt->bind_param('s', $tokenId);
$stmt->execute();
}
public function isRefreshTokenRevoked($tokenId)
{
global $conn;
$stmt = $conn->prepare('SELECT 1 FROM oauth_refresh_tokens WHERE refresh_token = ?');
$stmt->bind_param('s', $tokenId);
$stmt->execute();
$result = $stmt->get_result();
return $result->num_rows === 0;
}
public function getNewRefreshToken()
{
return new \OAuth\Entities\RefreshTokenEntity();
}
}
?>

View File

@ -0,0 +1,55 @@
<?php
namespace OAuth\Repositories;
use Config\Database;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use OAuth\Entities\ScopeEntity;
class ScopeRepository implements ScopeRepositoryInterface
{
protected $conn;
public function __construct()
{
$this->conn = Database::connect();
}
public function getScopeEntityByIdentifier($identifier)
{
$stmt = $this->conn->prepare('SELECT * FROM oauth_scopes WHERE scope = ?');
$stmt->bind_param('s', $identifier);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
return null;
}
$scope = $result->fetch_assoc();
$scopeEntity = new ScopeEntity();
$scopeEntity->setIdentifier($scope['scope']);
return $scopeEntity;
}
public function finalizeScopes(array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier = null)
{
if (count($scopes) === 0) {
$stmt = $this->conn->prepare('SELECT * FROM oauth_scopes WHERE is_default = 1');
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
$scope = new ScopeEntity();
$scope->setIdentifier($row['scope']);
$scopes[] = $scope;
}
}
return $scopes;
}
}
?>

View File

@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/../Controllers/AuthController.php';
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER["REQUEST_METHOD"];
$routes = [
'POST' => [
'/api_candidatos/oauth/token' => [AuthController::class, 'token']
]
];
if (isset($routes[$method][$path])) {
call_user_func($routes[$method][$path]);
} else {
header("HTTP/1.1 404 Not Found");
echo json_encode(['error' => 'Ruta no encontrada']);
exit;
}
?>

View File

@ -0,0 +1,35 @@
<?php
namespace Utils;
class CorsHandler {
/**
* Configura los encabezados CORS para todas las respuestas
*
* @param string $allowOrigin Origen permitido ('*' para todos)
* @param array $allowMethods Métodos HTTP permitidos
* @param array $allowHeaders Encabezados permitidos
* @param int $maxAge Tiempo de caché para las respuestas preflight (en segundos)
*/
public static function configureCors(
$allowOrigin = '*',
$allowMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
$allowHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'],
$maxAge = 86400
) {
// Configurar encabezados
header('Access-Control-Allow-Origin: ' . $allowOrigin);
header('Access-Control-Allow-Methods: ' . implode(', ', $allowMethods));
header('Access-Control-Allow-Headers: ' . implode(', ', $allowHeaders));
header('Access-Control-Max-Age: ' . $maxAge);
// Permitir enviar cookies en solicitudes CORS (si es necesario)
// header('Access-Control-Allow-Credentials: true');
// Manejar solicitudes preflight OPTIONS
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
header('Content-Type: application/json; charset=utf-8');
exit(0);
}
}
}
?>

View File

@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/../services/CandidatoService.php';
class CandidatoController {
public static function index () {
$candidatos = CandidatoService::getCandidatos();
header('Content-Type: application/json; charset=utf-8');
echo json_encode($candidatos, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
}
?>

View File

@ -0,0 +1,3 @@
<?php
require_once __DIR__ . '/routes/CandidatoRoutes.php';
?>

View File

@ -0,0 +1,50 @@
<?php
require_once __DIR__ . '/../../Config/Database.php';
use Config\Database;
class Candidato {
public static function getCandidatos() {
$conn = Database::connect();
$sql = "SELECT
c.id_candidato,
CONCAT(c.nombres, ' ', c.primer_apellido, ' ', IFNULL(c.segundo_apellido, '')) AS nombre_completo,
c.correo,
c.telefono,
g.descripcion AS genero,
r.descripcion AS rango_edad,
ti.descripcion AS tipo_identificacion,
ne.descripcion AS nivel_estudio,
gi.descripcion AS giro,
ic.nombre_empresa_institucion,
me.descripcion AS motivo_examen,
ic.calificacion_servicio,
ic.consentimiento_pub AS consentimiento_publicidad,
pa.nombre AS pais,
es.nombre AS estado,
mu.nombre AS municipio,
co.nombre AS colonia,
e.id_examen,
e.nombre_examen,
c.fecha_entrada,
c.fecha_salida
FROM candidato c
JOIN info_candidatos ic ON c.id_candidato = ic.id_candidato
JOIN genero g ON c.id_genero = g.id_genero
JOIN rango_edad r ON c.id_rango_edad = r.id_rango_edad
JOIN tipo_identificacion ti ON c.id_tipo_id = ti.id_tipo_id
JOIN examen e ON c.id_examen = e.id_examen
JOIN nivel_estudio ne ON ic.id_nivel = ne.id_nivel
JOIN giro gi ON ic.id_giro = gi.id_giro
JOIN paises pa ON ic.id_pais = pa.id
LEFT JOIN estados es ON ic.id_estado = es.id
LEFT JOIN municipios mu ON ic.id_municipio = mu.id
LEFT JOIN colonias co ON ic.id_colonia = co.id
LEFT JOIN motivo_examen me ON ic.id_motivo_examen = me.id;";
$result = $conn->query($sql);
return $result->fetch_all(MYSQLI_ASSOC);
}
}
?>

View File

@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/../controllers/CandidatoController.php';
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER["REQUEST_METHOD"];
$routes = [
'GET' => [
'/api_candidatos/candidatos/obtenerCandidatos' => [CandidatoController::class, 'index']
]
];
if (isset($routes[$method][$path])) {
call_user_func($routes[$method][$path]);
} else {
header("HTTP/1.1 404 Not Found");
echo "<h1>Error 404</h1><p>Ruta no encontrada: $path</p>";
}
?>

View File

@ -0,0 +1,52 @@
<?php
require_once __DIR__ . '/../models/Candidato.php';
class CandidatoService {
public static function getCandidatos () {
$rawCandidatos = Candidato::getCandidatos();
$candidatos = [];
foreach ($rawCandidatos as $candidato) {
$candidatos[] = [
"id_candidato" => $candidato["id_candidato"],
"nombre_completo" => $candidato["nombre_completo"],
"contacto" => [
"correo" => $candidato["correo"],
"telefono" => $candidato["telefono"]
],
"demograficos" => [
"genero" => $candidato["genero"],
"rango_edad" => $candidato["rango_edad"],
"tipo_identificacion" => $candidato["tipo_identificacion"],
"ubicacion" => [
"pais" => $candidato["pais"],
"estado" => $candidato["estado"],
"municipio" => $candidato["municipio"],
"colonia" => $candidato["colonia"]
]
],
"formacion" => [
"nivel_estudio" => $candidato["nivel_estudio"],
"giro" => $candidato["giro"],
"nombre_empresa_institucion" => $candidato["nombre_empresa_institucion"]
],
"examen" => [
"id_examen" => $candidato["id_examen"],
"nombre_examen" => $candidato["nombre_examen"],
"motivo" => $candidato["motivo_examen"]
],
"experiencia_servicio" => [
"calificacion_servicio" => (int)$candidato["calificacion_servicio"],
"consentimiento_publicidad" => $candidato["consentimiento_publicidad"] == "1"
],
"fechas" => [
"entrada" => $candidato["fecha_entrada"],
"salida" => $candidato["fecha_salida"]
]
];
}
return $candidatos;
}
}
?>

22
api_candidatos/vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitd32a508174e07df637170f00512a95ed::getLoader();

View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../defuse/php-encryption/bin/generate-defuse-key)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/defuse/php-encryption/bin/generate-defuse-key');
}
}
return include __DIR__ . '/..'.'/defuse/php-encryption/bin/generate-defuse-key';

View File

@ -0,0 +1,5 @@
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/generate-defuse-key
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
php "%BIN_TARGET%" %*

View File

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
api_candidatos/vendor/composer/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,15 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);

View File

@ -0,0 +1,11 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -0,0 +1,23 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'StellaMaris\\Clock\\' => array($vendorDir . '/stella-maris/clock/src'),
'Slim\\Psr7\\' => array($vendorDir . '/slim/psr7/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
'OAuth\\' => array($baseDir . '/src/OAuth'),
'League\\Uri\\' => array($vendorDir . '/league/uri/src', $vendorDir . '/league/uri-interfaces/src'),
'League\\OAuth2\\Server\\' => array($vendorDir . '/league/oauth2-server/src'),
'League\\Event\\' => array($vendorDir . '/league/event/src'),
'Lcobucci\\JWT\\' => array($vendorDir . '/lcobucci/jwt/src'),
'Lcobucci\\Clock\\' => array($vendorDir . '/lcobucci/clock/src'),
'Fig\\Http\\Message\\' => array($vendorDir . '/fig/http-message-util/src'),
'Defuse\\Crypto\\' => array($vendorDir . '/defuse/php-encryption/src'),
'Config\\' => array($baseDir . '/src/Config'),
);

View File

@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitd32a508174e07df637170f00512a95ed
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitd32a508174e07df637170f00512a95ed', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitd32a508174e07df637170f00512a95ed', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitd32a508174e07df637170f00512a95ed::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInitd32a508174e07df637170f00512a95ed::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

View File

@ -0,0 +1,131 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitd32a508174e07df637170f00512a95ed
{
public static $files = array (
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
'StellaMaris\\Clock\\' => 18,
'Slim\\Psr7\\' => 10,
),
'P' =>
array (
'Psr\\Http\\Message\\' => 17,
'Psr\\Clock\\' => 10,
),
'O' =>
array (
'OAuth\\' => 6,
),
'L' =>
array (
'League\\Uri\\' => 11,
'League\\OAuth2\\Server\\' => 21,
'League\\Event\\' => 13,
'Lcobucci\\JWT\\' => 13,
'Lcobucci\\Clock\\' => 15,
),
'F' =>
array (
'Fig\\Http\\Message\\' => 17,
),
'D' =>
array (
'Defuse\\Crypto\\' => 14,
),
'C' =>
array (
'Config\\' => 7,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'StellaMaris\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/stella-maris/clock/src',
),
'Slim\\Psr7\\' =>
array (
0 => __DIR__ . '/..' . '/slim/psr7/src',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Psr\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/psr/clock/src',
),
'OAuth\\' =>
array (
0 => __DIR__ . '/../..' . '/src/OAuth',
),
'League\\Uri\\' =>
array (
0 => __DIR__ . '/..' . '/league/uri/src',
1 => __DIR__ . '/..' . '/league/uri-interfaces/src',
),
'League\\OAuth2\\Server\\' =>
array (
0 => __DIR__ . '/..' . '/league/oauth2-server/src',
),
'League\\Event\\' =>
array (
0 => __DIR__ . '/..' . '/league/event/src',
),
'Lcobucci\\JWT\\' =>
array (
0 => __DIR__ . '/..' . '/lcobucci/jwt/src',
),
'Lcobucci\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/lcobucci/clock/src',
),
'Fig\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/fig/http-message-util/src',
),
'Defuse\\Crypto\\' =>
array (
0 => __DIR__ . '/..' . '/defuse/php-encryption/src',
),
'Config\\' =>
array (
0 => __DIR__ . '/../..' . '/src/Config',
),
);
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitd32a508174e07df637170f00512a95ed::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitd32a508174e07df637170f00512a95ed::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitd32a508174e07df637170f00512a95ed::$classMap;
}, null, ClassLoader::class);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,197 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '6bb919b212e576391c6c7e196f2955d79731b5ad',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '6bb919b212e576391c6c7e196f2955d79731b5ad',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'defuse/php-encryption' => array(
'pretty_version' => 'v2.4.0',
'version' => '2.4.0.0',
'reference' => 'f53396c2d34225064647a05ca76c1da9d99e5828',
'type' => 'library',
'install_path' => __DIR__ . '/../defuse/php-encryption',
'aliases' => array(),
'dev_requirement' => false,
),
'fig/http-message-util' => array(
'pretty_version' => '1.1.5',
'version' => '1.1.5.0',
'reference' => '9d94dc0154230ac39e5bf89398b324a86f63f765',
'type' => 'library',
'install_path' => __DIR__ . '/../fig/http-message-util',
'aliases' => array(),
'dev_requirement' => false,
),
'lcobucci/clock' => array(
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'reference' => 'c7aadcd6fd97ed9e199114269c0be3f335e38876',
'type' => 'library',
'install_path' => __DIR__ . '/../lcobucci/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'lcobucci/jwt' => array(
'pretty_version' => '4.0.4',
'version' => '4.0.4.0',
'reference' => '55564265fddf810504110bd68ca311932324b0e9',
'type' => 'library',
'install_path' => __DIR__ . '/../lcobucci/jwt',
'aliases' => array(),
'dev_requirement' => false,
),
'league/event' => array(
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'reference' => '062ebb450efbe9a09bc2478e89b7c933875b0935',
'type' => 'library',
'install_path' => __DIR__ . '/../league/event',
'aliases' => array(),
'dev_requirement' => false,
),
'league/oauth2-server' => array(
'pretty_version' => '8.4.3',
'version' => '8.4.3.0',
'reference' => '8a59a84450f0f64adcea8e1f8c0e0d9dfc8887d1',
'type' => 'library',
'install_path' => __DIR__ . '/../league/oauth2-server',
'aliases' => array(),
'dev_requirement' => false,
),
'league/oauth2server' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'league/uri' => array(
'pretty_version' => '6.8.0',
'version' => '6.8.0.0',
'reference' => 'a700b4656e4c54371b799ac61e300ab25a2d1d39',
'type' => 'library',
'install_path' => __DIR__ . '/../league/uri',
'aliases' => array(),
'dev_requirement' => false,
),
'league/uri-interfaces' => array(
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'reference' => '00e7e2943f76d8cb50c7dfdc2f6dee356e15e383',
'type' => 'library',
'install_path' => __DIR__ . '/../league/uri-interfaces',
'aliases' => array(),
'dev_requirement' => false,
),
'lncd/oauth2' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '*',
),
),
'paragonie/random_compat' => array(
'pretty_version' => 'v9.99.100',
'version' => '9.99.100.0',
'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
'type' => 'library',
'install_path' => __DIR__ . '/../paragonie/random_compat',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/clock' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/clock-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'psr/http-factory' => array(
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-factory',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-factory-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '^1.0',
),
),
'psr/http-message' => array(
'pretty_version' => '1.1',
'version' => '1.1.0.0',
'reference' => 'cb6ce4845ce34a8ad9e68117c10ee90a29919eba',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-message',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-message-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '^1.0 || ^2.0',
),
),
'ralouphie/getallheaders' => array(
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
'type' => 'library',
'install_path' => __DIR__ . '/../ralouphie/getallheaders',
'aliases' => array(),
'dev_requirement' => false,
),
'slim/psr7' => array(
'pretty_version' => '1.7.1',
'version' => '1.7.1.0',
'reference' => 'fe98653e7983010aa85c1d137c9b9ad5a1cd187d',
'type' => 'library',
'install_path' => __DIR__ . '/../slim/psr7',
'aliases' => array(),
'dev_requirement' => false,
),
'stella-maris/clock' => array(
'pretty_version' => '0.1.7',
'version' => '0.1.7.0',
'reference' => 'fa23ce16019289a18bb3446fdecd45befcdd94f8',
'type' => 'library',
'install_path' => __DIR__ . '/../stella-maris/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.32.0',
'version' => '1.32.0.0',
'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View File

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Taylor Hornby <https://defuse.ca>
Copyright (c) 2016 Paragon Initiative Enterprises <https://paragonie.com>.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,121 @@
php-encryption
===============
![Build Status](https://app.travis-ci.com/defuse/php-encryption.svg?branch=master)
[![codecov](https://codecov.io/gh/defuse/php-encryption/branch/master/graph/badge.svg)](https://codecov.io/gh/defuse/php-encryption)
[![Latest Stable Version](https://poser.pugx.org/defuse/php-encryption/v/stable)](https://packagist.org/packages/defuse/php-encryption)
[![License](https://poser.pugx.org/defuse/php-encryption/license)](https://packagist.org/packages/defuse/php-encryption)
[![Downloads](https://img.shields.io/packagist/dt/defuse/php-encryption.svg)](https://packagist.org/packages/defuse/php-encryption)
```terminal
composer require defuse/php-encryption
```
This is a library for encrypting data with a key or password in PHP. **It
requires PHP 5.6 or newer and OpenSSL 1.0.1 or newer.** We recommend using a
version of PHP that [still has security
support](https://www.php.net/supported-versions.php), which at the time of
writing means PHP 8.0 or later. Using this library with an unsupported
version of PHP could lead to security vulnerabilities.
The current version of `php-encryption` is v2.4.0. This library is expected to
remain stable and supported by its authors with security and bugfixes until at
least January 1st, 2024.
The library is a joint effort between [Taylor Hornby](https://defuse.ca/) and
[Scott Arciszewski](https://paragonie.com/blog/author/scott-arcizewski) as well
as numerous open-source contributors.
What separates this library from other PHP encryption libraries is, firstly,
that it is secure. The authors used to encounter insecure PHP encryption code on
a daily basis, so they created this library to bring more security to the
ecosystem. Secondly, this library is "difficult to misuse." Like
[libsodium](https://github.com/jedisct1/libsodium), its API is designed to be
easy to use in a secure way and hard to use in an insecure way.
Dependencies
------------
This library requires no special dependencies except for PHP 5.6 or newer with
the OpenSSL extensions (version 1.0.1 or later) enabled (this is the default).
It uses [random\_compat](https://github.com/paragonie/random_compat), which is
bundled in with this library so that your users will not need to follow any
special installation steps.
Getting Started
----------------
Start with the [**Tutorial**](docs/Tutorial.md). You can find instructions for
obtaining this library's code securely in the [Installing and
Verifying](docs/InstallingAndVerifying.md) documentation.
After you've read the tutorial and got the code, refer to the formal
documentation for each of the classes this library provides:
- [Crypto](docs/classes/Crypto.md)
- [File](docs/classes/File.md)
- [Key](docs/classes/Key.md)
- [KeyProtectedByPassword](docs/classes/KeyProtectedByPassword.md)
If you encounter difficulties, see the [FAQ](docs/FAQ.md) answers. The fixes to
the most commonly-reported problems are explained there.
If you're a cryptographer and want to understand the nitty-gritty details of how
this library works, look at the [Cryptography Details](docs/CryptoDetails.md)
documentation.
If you're interested in contributing to this library, see the [Internal
Developer Documentation](docs/InternalDeveloperDocs.md).
Other Language Support
----------------------
This library is intended for server-side PHP software that needs to encrypt data at rest.
If you are building software that needs to encrypt client-side, or building a system that
requires cross-platform encryption/decryption support, we strongly recommend using
[libsodium](https://download.libsodium.org/doc/bindings_for_other_languages) instead.
Examples
---------
If the documentation is not enough for you to understand how to use this
library, then you can look at an example project that uses this library:
- [encutil](https://github.com/defuse/encutil)
- [fileencrypt](https://github.com/tsusanka/fileencrypt)
Security Audit Status
---------------------
This code has not been subjected to a formal, paid, security audit. However, it
has received lots of review from members of the PHP security community, and the
authors are experienced with cryptography. In all likelihood, you are safer
using this library than almost any other encryption library for PHP.
If you use this library as a part of your business and would like to help fund
a formal audit, please [contact Taylor Hornby](https://defuse.ca/contact.htm).
Public Keys
------------
The GnuPG public key used to sign current and older releases is available in
[dist/signingkey.asc](https://github.com/defuse/php-encryption/raw/master/dist/signingkey.asc). Its fingerprint is:
```
2FA6 1D8D 99B9 2658 6BAC 3D53 385E E055 A129 1538
```
You can verify it against Taylor Hornby's [contact
page](https://defuse.ca/contact.htm) and
[twitter](https://twitter.com/DefuseSec/status/723741424253059074).
Due to the old key expiring, new releases will be signed with a new public key
available in [dist/signingkey-new.asc](https://github.com/defuse/php-encryption/raw/master/dist/signingkey-new.asc). Its fingerprint is:
```
6DD6 E677 0281 5846 FC85 25A3 DD2E 507F 7BDB 1669
```
A signature of this new key by the old key is available in
[dist/signingkey-new.asc.sig](https://github.com/defuse/php-encryption/raw/master/dist/signingkey-new.asc.sig).

View File

@ -0,0 +1,14 @@
#!/usr/bin/env php
<?php
use Defuse\Crypto\Key;
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
if (file_exists($file)) {
require $file;
break;
}
}
$key = Key::createNewRandomKey();
echo $key->saveToAsciiSafeString(), "\n";

View File

@ -0,0 +1,35 @@
{
"name": "defuse/php-encryption",
"description": "Secure PHP Encryption Library",
"license": "MIT",
"keywords": ["security", "encryption", "AES", "openssl", "cipher", "cryptography", "symmetric key cryptography", "crypto", "encrypt", "authenticated encryption"],
"authors": [
{
"name": "Taylor Hornby",
"email": "taylor@defuse.ca",
"homepage": "https://defuse.ca/"
},
{
"name": "Scott Arciszewski",
"email": "info@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"autoload": {
"psr-4": {
"Defuse\\Crypto\\": "src"
}
},
"require": {
"paragonie/random_compat": ">= 2",
"ext-openssl": "*",
"php": ">=5.6.0"
},
"require-dev": {
"yoast/phpunit-polyfills": "^2.0.0",
"phpunit/phpunit": "^5|^6|^7|^8|^9|^10"
},
"bin": [
"bin/generate-defuse-key"
]
}

View File

@ -0,0 +1,39 @@
# This builds defuse-crypto.phar. To run this Makefile, `box` and `composer`
# must be installed and in your $PATH. Run it from inside the dist/ directory.
box := $(shell which box)
composer := $(shell which composer)
gitcommit := $(shell git rev-parse HEAD)
.PHONY: all
all: build-phar
.PHONY: sign-phar
sign-phar:
gpg -u DD2E507F7BDB1669 --armor --output defuse-crypto.phar.sig --detach-sig defuse-crypto.phar
# ensure we run in clean tree. export git tree and run there.
.PHONY: build-phar
build-phar:
@echo "Creating .phar from revision $(shell git rev-parse HEAD)."
rm -rf worktree
install -d worktree
(cd $(CURDIR)/..; git archive HEAD) | tar -x -C worktree
$(MAKE) -f $(CURDIR)/Makefile -C worktree defuse-crypto.phar
mv worktree/*.phar .
rm -rf worktree
.PHONY: clean
clean:
rm -vf defuse-crypto.phar defuse-crypto.phar.sig
# Inside workdir/:
defuse-crypto.phar: dist/box.json composer.lock
cp dist/box.json .
php $(box) compile -c box.json -v
composer.lock:
$(composer) config autoloader-suffix $(gitcommit)
$(composer) install --no-dev

View File

@ -0,0 +1,22 @@
{
"chmod": "0755",
"finder": [
{
"in": "src",
"name": "*.php"
},
{
"in": "vendor/composer",
"name": "*.php"
},
{
"in": "vendor/paragonie",
"name": "*.php",
"exclude": "other"
}
],
"main": "vendor/autoload.php",
"output": "defuse-crypto.phar",
"shebang": false,
"stub": true
}

View File

@ -0,0 +1,4 @@
<?php
require 'defuse-crypto.phar';
require realpath(dirname(__FILE__) . '/../vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php');
?>

View File

@ -0,0 +1,53 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBF5V4TABEAC4G2BkHDaqbip3gj1oOqKh3V6LQa9QAd/f/hyhmR5hXpciPxf3
NNHAxzoGAuB51f1YPJTNO59mGKHDuCFfr0pI94HDGoW4WgxqnUyqHBj2+/JhQPqO
lgDT0QDcfxmUd0wfZl/Ur+8SsaBYvfFWNmPaXHp9m4MMRtw9uZNIW6LlZ24JqmGy
/YUELUSH7P+uJ4HQEdixaqQ0VgIomRDI+5IwdJMtq4TSNazQm3nNmH9Em37cdi6J
NDfFRy2QxJDmuqlg8mkpS5TvrrNy/UJwIeXO9PuGaBODr8GAKWvhkpfGlxN+hWMY
01bOFnuEnOcuXw8BjPAKHqwOuGvinNmQ7lX1Rj3ssd31sTUimop0oNjOTZztpJBR
m6wO2/YGMjt+eL02NgBBDIsV837PeWuJmymTJDGQuBjZ3JWUfyT3AnkA8OU5vKjs
pM8AjIiuU7C8zQhUSHDnukTKWpBmMdOXeWNb5Ye6n60wJWzWFGlm+cYalPs+q3H8
bxHxHEdFT0rUpxB05bc9zsZ3gGkc2NTNW/00a6gvTyX1UsBAeNgvVSHBHQGfow6o
ZKG+LnVxd+cl97ay6kP29eLypXffbXQ3hMXe9tUNBjAeiok9tssU70Epr9wTh/Fm
/iEbGc8VhS4TSk3c+3eS16rvlQ51FmAlmG6kAnN/ah+BiM4syPrWcJHIDQARAQAB
tG1kZWZ1c2UvcGhwLWVuY3J5cHRpb24gbWFpbnRhaW5lcnMgKGRlZnVzZS9waHAt
ZW5jcnlwdGlvbiByZWxlYXNlIHNpZ25pbmcga2V5KSA8cGhwLWVuY3J5cHRpb25A
ZGVmdXNlLmludmFsaWQ+iQJUBBMBCAA+FiEEbdbmdwKBWEb8hSWj3S5Qf3vbFmkF
Al5V4TACGwMFCQlmAYAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ3S5Qf3vb
FmnQ4Q//bHAwDI7CcTlDDktdRCP0YCRtb5zMa6vSqnZLi5aTqzmL1yQCAp1/JTwf
nlHn5xt60eKwfjIKj7Kj0n8WDFYnlOu30H5fNtFHis0xeS7GkH60tIE/pQUZILlB
Wcnx/ZPnlxccjtfSbnpelSPdvIoHVRNhh1ZYG/49kuuv8TMbMIi2FBAzGEatPoLN
f4wntoOKGvl8R2rPc2geapXTz++X+HJkddHCISR61obDRl9P0t9x+0M7gGSVMGfC
GC4wh3JB6/ha8l+DI+l88/64IKRG+M33bBwLGQ0RIhotHIy442gLJTm6TeoH8iUz
xCBwPYW+Ta1wZi17PIjHdTkNGBeEj/Hr5tTVV3oxrQVgHCymzasnz9IwcCCMwpKK
ZOMFl0+PT3TSBKLnUByvOB64YOjxU7t+sRf53Biz3yKzto5VdHGW64OG2vGFy/Xz
vI5RqU34wjtEHxWfz8y2GBnhD2TzEFCIIWPAX3TDG64NBSEBjhUraOmoVoaYJlP6
rqxIQo4yhC+f5rnr2ZA48Hnrg0jEdVvN07FegoOnQQPpYBBkOrkTDWChn8oiXMfg
9bjv19zDOBVXl9EU+P8AhwTHz/pBKmhb97N9nYp/pbmejA+I0Kw1vZo7gaMxL938
oQkdtWT70ZzcpcZfeKVXoZa/ddAmuxzNknZA8ZnjQ9Qhv7aNX2O5Ag0EXlXhMAEQ
AM8od4/85i7ZPmM6C1M4n4XcXeKsuZKHLvLLcRHFGkjVdXRSaxpbk2yDJiLnB9hX
GSJG2gUCT+yrimjQ71bJ4q9K2+mkVHVjdtCrCtoOYEIpMLzsRtqyAWotcVmdv8Zv
4IIjxfdxpTkj9gZmUfDIe6tbN2iBCAo1HArXq1qSdof3ui8SqdWeinkd7lZMesFm
dGQaAcHbmEakO5mRzljme8IBs3UY9j/zxEG1JbsHx9ua7CVwJ7lxi2SgSW6nF9k5
CX5zbrDqlqSJNtDs+KbjCbI2eK+qe4qZWHPiw4bNBn6EWf97/4Os8w7Vrrpyd2eO
1JENwjlG6WG9mbJdIWWwakZ0CeH5LnJo6dV47KZbbbB6ncavaL+VpfbTCgdOGsCc
GcYUVl90/v5pPm2owx4Dg9hSfcp8fesQuq4b79NAcjF7meu5wgNdvFlfuXony+UC
W2wNi0mi9lzLD0n0j0GDzWyd3r7yXmPTL4LhrQu/pIcWIljKI3GUAQZqIYbGAO3G
7hEFT8rDWg2vKRtMag4iy5FvZFqR+7TwWJAcWnHJBZ95F9NzeYIFhp9a3hxbKXqD
xEnyGgzAszUycq29BApT4/4rDZQuXuOBd4lJp8tSzctLjvo7D3la+MWD6AlDkYT4
bGKN9NfRCzYr2Zq3jOByAV3d5hGgyzdJlZSqXAGtbHHdABEBAAGJAjwEGAEIACYW
IQRt1uZ3AoFYRvyFJaPdLlB/e9sWaQUCXlXhMAIbDAUJCWYBgAAKCRDdLlB/e9sW
aSGfD/wPeq6lGu8ocHIkO74VPioJRKRXDVLsY02xKP64p0RHUGFTOqqB3A3UV0ue
tkizoUdfF5xkgJ18gbxXo8lotBq+Ita5hoYAfqJnTnucAPGovREJ+X1HfdK4pJqW
KNJElBz+fC4chqksiUAuH7IMImmy0/lA+LqZagzkQJU10MvmiFZ6kn+X5Mb4izRl
vAHo16eI4psApdT8Bs7mwAjgCHxS9Re46uOElB4Bx3iFPd/PEwHWnfr8x9TJZYKW
fsShG31+vfBRCfGtfKGxiAkp3EEM11lzbbfMcC3lai5iJQ/FmHgoIDeIG2Ebuk4w
/PYakSrpvkEYoMP31pVHDhzopVeURS2lpvQJ4CvTP5CVQtKrbuygow6GF8N/drCE
hdEx22pzW02ADS9fgzrlDztIOlOvC9a+epISIaEjfrc9dWhrw6chZEoWIil2MVQR
Sj0jZ8w/H7P88oHTOcFVel73ZEPg9eRUkqMnIn3DWUuqLI2SX/AtVnhdYHWTiOkq
knsGofWxUSu3RZR2ZElK9hjNKdVbGDzHGAYeJihieTKIOXpCf6Ix5B32tmFpfmBV
Q9YP3JLsRTxIMbXsJImand/r6fSjdmTpk2PovYPtE1HTJKaVHeagQdsrWw5LaJv0
ZWuwJm0y0WJXcAEjwOHhBs0nvq2CXuZi2ZTPtY+DbsSFWhaN7g==
=Ysgx
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2
mQINBFarvO4BEACdQBaLt6SUBx1cB5liUu1qo+YwVLh9bxTregQtmEREMdTVqXYt
e5b79uL4pQp2GlKHcEyRURS+6rIIruM0oh9ZYGTJYPAkCDzJxaU2awZeFbfBvpCm
iF66/O4ZJI4mlT8dFKmxBJxDhfeOR2UmmhDiEsJK9FxBKUzvo/dWrX2pBzf8Y122
iIaVraSo+tymaf7vriaIf/NnSKhDw8dtQYGM4NMrxxsPTfbCF8XiboDgTkoD2A+6
NpOJYxA4Veedsf2TP9YLhljH4m5yYlfjjqBzbBCPWuE6Hhy5Xze9mncgDr7LKenm
Ctf2NxW6y4O3RCI+9eLlBfFWB+KuGV87/b5daetX7NNLbjID8z2rqEa+d6wu5xA5
Ta2uiVkAOEovr3XnkayZ9zth+Za7w7Ai0ln0N/LVMkM+Gu4z/pJv6HjmTGDM2wJb
fs+UOM0TFdg+N81Do67XT2M4o0MeHyUqsIiWpYa2Qf1PNmqTQNJnRk8uZZ9I96Nh
eCgNuCbhsQiYBMicox+xmuWAlGAfA06y0kCtmqGhiBGArdJlWvUqPqGiZ4Hln9z0
FJmXDOh0Q/FIPxcDg8mKRRbx+lOP389PLsPpj4b2B/4PEgfpCCOwuKpLotATZxC1
9JwFk0Y/cvUUkq4a+nAJBNtBbtRJkEesuuUnRq6XexmnE3uUucDcV0XCSwARAQAB
tCBUYXlsb3IgSG9ybmJ5IDx0YXlsb3JAZGVmdXNlLmNhPokCPQQTAQgAJwUCVqu8
7gIbAwUJB4TOAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRA4XuBVoSkVOJbx
EACG0F9blPMAsK05EWyNnnS4mw25zPfbaqqEvYbquAeM0nBpRDm7sRn2MNR0AL4g
7XrtxE/4qYkdEl6f2wFCQeRhZgxE3w22llredzLme11Hic8hn4i7ysdIw0r9dMMR
kjgR5UcWpv8iU847czyK09PkKW2EaLRbX2qbA7rNU5qCFKeD4Sy4bBTteISeVsHo
Vr9o1/bRrMhgZ++ts8hYf0LmujIf5cxp+qcdKwCXSnS/gmmXaKRMCPv/Wdlq9bt6
LX9jZB9lXBdGxcBJeFOsTG+QRDiVjg3d6i3o3TAKV87ALBI4v2ADEYtN8lviHo3/
SovVKv6zrUsZHxhoGiLTiksNrYsKMmiMxdJCoOazmtUPnZ4UOtT8NdqMPoKvdoRz
f4rhZ+f5jSVD9OuX2PDmfyq21Rdiym7Vcgr+uTIFJ3ShRHjWb/ytCwoB2FeGY6+G
AKY58bTQvUIqEJvSov/+TAqZ4BfOuSdTLcHglV1OdUu2SFZvU2gmyVp0l5elGv5t
FyUlBJUkQT9MtvsdLOR7vQi8QapV+9LWpqwvaj9hyEJz848DQ2sdYTphUytFHv7H
k58DAtVhTrVjHyeefjiYtMl6vSAgTjy5LWAUpo5TfhdGrAi0Tdd/GD7amHoWoDy8
EKXKq2xPLo3JOdkWYQUi5NErzEskfsSzpCOgyDJmGetWK7kCDQRWq7zuARAAu7/i
cm8cjgLhHEX/bgfwOT2hLOLSjjve0O8YFSuJO9XqIHXqmfVOrqWtfG0Mh4bwlfqc
MAvBfF5NSSPfAE4ftBAQ1e5jEv8hJeqICpq3IHTFX4etBs49NfNkyveQl/amVTu1
+/O5J4CuIcsEf3y0Xuu38n7EB3SfMQCWLcOR1NyZoX3bI+CGRpOVVoFse3ljSWL4
LhLVl0WiEMXULsussEoN+c6x9KCyAi/jFOrxrTrFC//sZesKj6KucoqKGfwMWrrv
IeRT9Ga8Wn5MJnQu0aWg+zVVYqTedXZLNLODgQIInFnXO0seBXy15yDok1y5bkx2
sinKg4+mueYaGUpoUti0hM3J3yaC34i6Cwa8MQoLNw1JIS/oNtKjpMxyV10w8aoc
PHRK3n7UYp10mJHx7aM+lldSKvVS1NTQmI4vloNLwMp324H5ANDFAlRUz7mysVnu
DEEvigPSPxs5ZYENu/i7pCQC5qHfhrlBrQwTjhegr0pQPcumy2fO5SGC9l/5B7ev
bqQSZmDeWWoTvh2w2wl5/RWAsgZKx6rDtkCqYx7sSBY17uorrxP24LP4zhq7NxRV
nfdsLogbCFNVQ66u7qvq5zFccdFtg9h1HQWdS7wbnKSBGZoo5gl6js7GGtxfGbb0
oQ9kp6eciF4U92r6POhVgbRe4CfPo50nqgZBddkAEQEAAYkCJQQYAQgADwUCVqu8
7gIbDAUJB4TOAAAKCRA4XuBVoSkVOFJ8D/9J8IJ4XWUU3FYIaHJ3XeSoxDmTi7d5
WmNdf1lmwz82MQjG4uw17oCbvQzmj4/a/CM1Ly4v0WwBhUf9aiNErD0ByHASFnuc
tlQBLVJdk0vRyD0fZakGg64qCA76hiySjMhlGHkQFyP2mDORc2GNu/OqFGm79pXT
ZUplXxd431E603/agM5xJrweutMMpP1nBFTSEMJvbMNzDVN8I1A1CH4zVmAVxOUk
sQ5L5rXW+KeXWyiMF24+l2CMnkQ2CxfHpkcpfPJs1Cbt+TIBSSofIqK8QJXrb/2f
Zpl/ftqW7Xe86rJFrB/Y/77LDWx10rqWEvfCqrBxrMj7ONAQfbKQF/IjAwDN17Wf
1K74rqKnRu+KHCyNM89s1iDbQC9kzZfzYt4AEOvAH/ZQDMZffzPSbnfkBerExFpa
93XMuiR66jiBsf9IXIQeydpJD4Ogl2sSUSxFEJxJ/bBSxPxC5w7/BVMA7Am1y8Zo
3hrpqnX2PBzxG7L0FZ6fYkfR3p8JS7vI6nByBf2IDv8W32wn43olPf+u6uobHLvt
ttapOjwPAhPDalRuxs9U6WSg06QJkT/0F8TFUPWpsFmKTl+G4Ty7PHWsjeeNHJCL
7/5RQboFY3k8Jy3/sIofABO6Un9LJivDuu9PxqA0IgvaS6Mja8JdCCk9Nyk4vHB7
IEgAL/CYqrk38w==
=lmD7
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,64 @@
Cryptography Details
=====================
Here is a high-level description of how this library works. Any discrepancy
between this documentation and the actual implementation will be considered
a security bug.
Let's start with the following definitions:
- HKDF-SHA256(*k*, *n*, *info*, *s*) is the key derivation function specified in
RFC 5869 (using the SHA256 hash function). The parameters are:
- *k*: The initial keying material.
- *n*: The number of output bytes.
- *info*: The info string.
- *s*: The salt.
- AES-256-CTR(*m*, *k*, *iv*) is AES-256 encryption in CTR mode. The parameters
are:
- *m*: An arbitrary-length (possibly zero-length) message.
- *k*: A 32-byte key.
- *iv*: A 16-byte initialization vector (nonce).
- PBKDF2-SHA256(*p*, *s*, *i*, *n*) is the password-based key derivation
function defined in RFC 2898 (using the SHA256 hash function). The parameters
are:
- *p*: The password string.
- *s*: The salt string.
- *i*: The iteration count.
- *n*: The output length in bytes.
- VERSION is the string `"\xDE\xF5\x02\x00"`.
- AUTHINFO is the string `"DefusePHP|V2|KeyForAuthentication"`.
- ENCRINFO is the string `"DefusePHP|V2|KeyForEncryption"`.
To encrypt a message *m* using a 32-byte key *k*, the following steps are taken:
1. Generate a random 32-byte string *salt*.
2. Derive the 32-byte authentication key *akey* = HKDF-SHA256(*k*, 32, AUTHINFO, *salt*).
3. Derive the 32-byte encryption key *ekey* = HKDF-SHA256(*k*, 32, ENCRINFO, *salt*).
4. Generate a random 16-byte initialization vector *iv*.
5. Compute *c* = AES-256-CTR(*m*, *ekey*, *iv*).
6. Combine *ctxt* = VERSION || *salt* || *iv* || *c*.
7. Compute *h* = HMAC-SHA256(*ctxt*, *akey*).
8. Output *ctxt* || *h*.
Decryption is roughly the reverse process (see the code for details, since the
security of the decryption routine is highly implementation-dependent).
For encryption using a password *p*, steps 1-3 above are replaced by:
1. Generate a random 32-byte string *salt*.
2. Compute *k* = PBKDF2-SHA256(SHA256(*p*), *salt*, 100000, 32).
3. Derive the 32-byte authentication key *akey* = HKDF-SHA256(*k*, 32, AUTHINFO, *salt*)
4. Derive the 32-byte encryption key *ekey* = HKDF-SHA256(*k*, 32, ENCRINFO, *salt*)
The remainder of the process is the same. Notice the reuse of the same *salt*
for PBKDF2-SHA256 and HKDF-SHA256. The prehashing of the password in step 2 is
done to prevent a [DoS attack using long
passwords](https://github.com/defuse/php-encryption/issues/230).
For `KeyProtectedByPassword`, the serialized key is encrypted according to the
password encryption defined above. However, the actual password used for
encryption is the SHA256 hash of the password the user provided. This is done in
order to provide domain separation between the message encryption in the user's
application and the internal key encryption done by this library. It fixes
a [key replacement chosen-protocol
attack](https://github.com/defuse/php-encryption/issues/240).

View File

@ -0,0 +1,51 @@
Frequently Asked Questions
===========================
How do I use this library to encrypt passwords?
------------------------------------------------
Passwords should not be encrypted, they should be hashed with a *slow* password
hashing function that's designed to slow down password guessing attacks. See
[How to Safely Store Your Users' Passwords in
2016](https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016).
How do I give it the same key every time instead of a new random key?
----------------------------------------------------------------------
A `Key` object can be saved to a string by calling its `saveToAsciiSafeString()`
method. You will have to save that string somewhere safe, and then load it back
into a `Key` object using `Key`'s `loadFromAsciiSafeString` static method.
Where you store the string depends on your application. For example if you are
using `KeyProtectedByPassword` to encrypt files with a user's login password,
then you should not store the `Key` at all. If you are protecting sensitive data
on a server that may be compromised, then you should store it in a hardware
security module. When in doubt, consult a security expert.
Why is an EnvironmentIsBrokenException getting thrown?
-------------------------------------------------------
Either you've encountered a bug in this library, or your system doesn't support
the use of this library. For example, if your system does not have a secure
random number generator, this library will refuse to run, by throwing that
exception, instead of falling back to an insecure random number generator.
Why am I getting a BadFormatException when loading a Key from a string?
------------------------------------------------------------------------
If you're getting this exception, then the string you're giving to
`loadFromAsciiSafeString()` is *not* the same as the string you got from
`saveToAsciiSafeString()`. Perhaps your database column isn't wide enough and
it's truncating the string as you insert it?
Does encrypting hide the length of the plaintext?
--------------------------------------------------
Encryption does not, and is not intended to, hide the length of the data being
encrypted. For example, it is not safe to encrypt a field in which only a small
number of different-length values are possible (e.g. "male" or "female") since
it would be possible to tell what the plaintext is by looking at the length of
the ciphertext. In order to do this safely, it is your responsibility to, before
encrypting, pad the data out to the length of the longest string that will ever
be encrypted. This way, all plaintexts are the same length, and no information
about the plaintext can be gleaned from the length of the ciphertext.

View File

@ -0,0 +1,53 @@
Getting The Code
=================
There are two ways to use this library in your applications. You can either:
1. Use [Composer](https://getcomposer.org/), or
2. `require_once` a single `.phar` file in your application.
If you are not using either option (for example, because you're using Git submodules), you may need to write your own autoloader ([example](https://gist.github.com/paragonie-scott/949daee819bb7f19c50e5e103170b400)).
Option 1: Using Composer
-------------------------
Run this inside the directory of your composer-enabled project:
```sh
composer require defuse/php-encryption
```
Unfortunately, composer doesn't provide a way for you to verify that the code
you're getting was signed by this library's authors. If you want a more secure
option, use the `.phar` method described below.
Option 2: Including a PHAR
----------------------------
The `.phar` option lets you include this library into your project simply by
calling `require_once` on a single file. Download `defuse-crypto.phar` and
`defuse-crypto.phar.sig` from this project's
[releases](https://github.com/defuse/php-encryption/releases) page.
You should verify the integrity of the `.phar`. The `defuse-crypto.phar.sig`
contains the signature of `defuse-crypto.phar`. It is signed with Taylor
Hornby's PGP key. You can find Taylor's public key in `dist/signingkey.asc`. You
can verify the public key's fingerprint against the Taylor Hornby's [contact
page](https://defuse.ca/contact.htm) and
[twitter](https://twitter.com/DefuseSec/status/723741424253059074).
Once you have verified the signature, it is safe to use the `.phar`. Place it
somewhere in your file system, e.g. `/var/www/lib/defuse-crypto.phar`, and then
pass that path to `require_once`.
```php
<?php
require_once('/var/www/lib/defuse-crypto.phar');
// ... the Crypto, File, Key, and KeyProtectedByPassword classes are now
// available ...
// ...
```

View File

@ -0,0 +1,166 @@
Information for the Developers of php-encryption
=================================================
Status
-------
This library is currently frozen under a long-term support release. We do not
plan to add any new features. We will maintain the library by fixing any bugs
that are reported, or security vulnerabilities that are found.
Development Environment
------------------------
Development is done on Linux. To run the tests, you will need to have the
following tools installed:
- `php` (with OpenSSL enabled, if you're compiling from source).
- `gpg`
- `composer`
Running the Tests
------------------
First do `composer install` and then you can run the tests by running
`./test.sh`. This will download a PHPUnit PHAR, verify its cryptographic
signatures, and then use it to run the tests in `test/unit`.
Getting and Using Psalm
-----------------------
[Psalm](https://github.com/vimeo/psalm) is a static analysis suite for PHP projects.
We use Psalm to ensure type safety throughout our library.
To install Psalm, you just need to run one command:
composer require --dev "vimeo/psalm:dev-master"
To verify that your code changes are still strictly type-safe, run the following
command:
vendor/bin/psalm
Reporting Bugs
---------------
Please report bugs, even critical security vulnerabilities, by opening an issue
on GitHub. We recommend disclosing security vulnerabilities found in this
library *publicly* as soon as possible.
Philosophy
-----------
This library is developed around several core values:
- Rule #1: Security is prioritized over everything else.
> Whenever there is a conflict between security and some other property,
> security will be favored. For example, the library has runtime tests,
> which make it slower, but will hopefully stop it from encrypting stuff
> if the platform it's running on is broken.
- Rule #2: It should be difficult to misuse the library.
> We assume the developers using this library have no experience with
> cryptography. We only assume that they know that the "key" is something
> you need to encrypt and decrypt the messages, and that it must be kept
> secret. Whenever possible, the library should refuse to encrypt or decrypt
> messages when it is not being used correctly.
- Rule #3: The library aims only to be compatible with itself.
> Other PHP encryption libraries try to support every possible type of
> encryption, even the insecure ones (e.g. ECB mode). Because there are so
> many options, inexperienced developers must decide whether to use "CBC
> mode" or "ECB mode" when both are meaningless terms to them. This
> inevitably leads to vulnerabilities.
> This library will only support one secure mode. A developer using this
> library will call "encrypt" and "decrypt" methods without worrying about
> how they are implemented.
- Rule #4: The library should require no special installation.
> Some PHP encryption libraries, like libsodium-php, are not straightforward
> to install and cannot packaged with "just download and extract"
> applications. This library will always be just a handful of PHP files that
> you can copy to your source tree and require().
Publishing Releases
--------------------
To make a release, you will need to install [composer](https://getcomposer.org/)
and [box](https://github.com/box-project/box2) on your system. They will need to
be available in your `$PATH` so that running the commands `composer` and `box`
in your terminal run them, respectively. You will also need the private key for
signing (ID: 7B4B2D98) available.
Once you have those tools installed and the key available follow these steps:
**Remember to set the version number in `composer.json`!**
Make a fresh clone of the repository:
```
git clone <url>
```
Check out the branch you want to release:
```
git checkout <branchname>
```
Check that the version number in composer.json is correct (or not specified so that it gets picked up from the git tag):
```
cat composer.json
```
Check that the version number and support lifetime in README.md are correct:
```
cat README.md
```
Run the tests:
```
composer install
./test.sh
```
Generate the `.phar`:
```
cd dist
make build-phar
```
Test the `.phar`:
```
cd ../
./test.sh dist/phar-testing-autoload.php
```
Sign the `.phar`:
```
cd dist
make sign-phar
```
Tag the release:
```
git -c user.signingkey=DD2E507F7BDB1669 tag -s "<TAG NAME>" -m "<TAG MESSAGE>"
```
`<TAG NAME>` should be in the format `v2.0.0` and `<TAG MESSAGE>` should look
like "Release of v2.0.0."
Push the tag to github, then use the
[releases](https://github.com/defuse/php-encryption/releases) page to draft
a new release for that tag. Upload the `.phar` and the `.phar.sig` file to be
included as part of that release.

View File

@ -0,0 +1,314 @@
Tutorial
=========
Hello! If you're reading this file, it's because you want to add encryption to
one of your PHP projects. My job, as the person writing this documentation, is
to help you make sure you're doing the right thing and then show you how to use
this library to do it. To help me help you, please read the documentation
*carefully* and *deliberately*.
A Word of Caution
------------------
Encryption is not magic dust you can sprinkle on a system to make it more
secure. The way encryption is integrated into a system's design needs to be
carefully thought out. Sometimes, encryption is the wrong thing to use. Other
times, encryption needs to be used in a very specific way in order for it to
work as intended. Even if you are sure of what you are doing, we strongly
recommend seeking advice from an expert.
The first step is to think about your application's threat model. Ask yourself
the following questions. Who will want to attack my application, and what will
they get out of it? Are they trying to steal some information? Trying to alter
or destroy some information? Or just trying to make the system go down so people
can't access it? Then ask yourself how encryption can help combat those threats.
If you're going to add encryption to your application, you should have a very
clear idea of exactly which kinds of attacks it's helping to secure your
application against. Once you have your threat model, think about what kinds of
attacks it *does not* cover, and whether or not you should improve your threat
model to include those attacks.
**This isn't for storing user login passwords:** The most common use of
cryptography in web applications is to protect the users' login passwords. If
you're trying to use this library to "encrypt" your users' passwords, you're in
the wrong place. Passwords shouldn't be *encrypted*, they should be *hashed*
with a slow computation-heavy function that makes password guessing attacks more
expensive. See [How to Safely Store Your Users' Passwords in
2016](https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016).
**This isn't for encrypting network communication:** Likewise, if you're trying
to encrypt messages sent between two parties over the Internet, you don't want
to be using this library. For that, set up a TLS connection between the two
points, or, if it's a chat app, use the [Signal
Protocol](https://whispersystems.org/blog/advanced-ratcheting/).
What this library provides is symmetric encryption for "data at rest." This
means it is not suitable for use in building protocols where "data is in motion"
(i.e. moving over a network) except in limited set of cases.
Please note that **encryption does not, and is not intended to, hide the
*length* of the data being encrypted.** For example, it is not safe to encrypt
a field in which only a small number of different-length values are possible
(e.g. "male" or "female") since it would be possible to tell what the plaintext
is by looking at the length of the ciphertext. In order to do this safely, it is
your responsibility to, before encrypting, pad the data out to the length of the
longest string that will ever be encrypted. This way, all plaintexts are the
same length, and no information about the plaintext can be gleaned from the
length of the ciphertext.
Getting the Code
-----------------
There are several different ways to obtain this library's code and to add it to
your project. Even if you've already cloned the code from GitHub, you should
take steps to verify the cryptographic signatures to make sure the code you got
was not intercepted and modified by an attacker.
Please head over to the [**Installing and
Verifying**](InstallingAndVerifying.md) documentation to get the code, and then
come back here to continue the tutorial.
Using the Library
------------------
I'm going to assume you know what symmetric encryption is, and the difference
between symmetric and asymmetric encryption. If you don't, I recommend taking
[Dan Boneh's Cryptography I course](https://www.coursera.org/learn/crypto/) on
Coursera.
To give you a quick introduction to the library, I'm going to explain how it
would be used in two sterotypical scenarios. Hopefully, one of these sterotypes
is close enough to what you want to do that you'll be able to figure out what
needs to be different on your own.
### Formal Documentation
While this tutorial should get you up and running fast, it's important to
understand how this library behaves. Please make sure to read the formal
documentation of all of the functions you're using, since there are some
important security warnings there.
The following classes are available for you to use:
- [Crypto](classes/Crypto.md): Encrypting and decrypting strings.
- [File](classes/File.md): Encrypting and decrypting files.
- [Key](classes/Key.md): Represents a secret encryption key.
- [KeyProtectedByPassword](classes/KeyProtectedByPassword.md): Represents
a secret encryption key that needs to be "unlocked" by a password before it
can be used.
### Scenario #1: Keep data secret from the database administrator
In this scenario, our threat model is as follows. Alice is a server
administrator responsible for managing a trusted web server. Eve is a database
administrator responsible for managing a database server. Dave is a web
developer working on code that will eventually run on the trusted web server.
Let's say Alice and Dave trust each other, and Alice is going to host Dave's
application on her server. But both Alice and Dave don't trust Eve. They know
Eve is a good database administrator, but she might have incentive to steal the
data from the database. They want to keep some of the web application's data
secret from Eve.
In order to do that, Alice will use the included `generate-defuse-key` script
which generates a random encryption key and prints it to standard output:
```sh
$ composer require defuse/php-encryption
$ vendor/bin/generate-defuse-key
```
Alice will run this script once and save the output to a configuration file, say
in `/etc/daveapp-secret-key.txt` and set the file permissions so that only the
user that the website PHP scripts run as can access it.
Dave will write his code to load the key from the configuration file:
```php
<?php
use Defuse\Crypto\Key;
function loadEncryptionKeyFromConfig()
{
$keyAscii = // ... load the contents of /etc/daveapp-secret-key.txt
return Key::loadFromAsciiSafeString($keyAscii);
}
```
Then, whenever Dave wants to save a secret value to the database, he will first
encrypt it:
```php
<?php
use Defuse\Crypto\Crypto;
// ...
$key = loadEncryptionKeyFromConfig();
// ...
$ciphertext = Crypto::encrypt($secret_data, $key);
// ... save $ciphertext into the database ...
```
Whenever Dave wants to get the value back from the database, he must decrypt it
using the same key:
```php
<?php
use Defuse\Crypto\Crypto;
// ...
$key = loadEncryptionKeyFromConfig();
// ...
$ciphertext = // ... load $ciphertext from the database
try {
$secret_data = Crypto::decrypt($ciphertext, $key);
} catch (\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
// An attack! Either the wrong key was loaded, or the ciphertext has
// changed since it was created -- either corrupted in the database or
// intentionally modified by Eve trying to carry out an attack.
// ... handle this case in a way that's suitable to your application ...
}
```
Note that if anyone ever steals the key from Alice's server, they can decrypt
all of the ciphertexts that are stored in the database. As part of our threat
model, we are assuming Alice's server administration skills and Dave's secure
coding skills are good enough to stop Eve from being able to steal the key.
Under those assumptions, this solution will prevent Eve from seeing data that's
stored in the database.
However, notice that our threat model says nothing about what could happen if
Eve wants to *modify* the data. With this solution, Eve will not be able to
alter any individual ciphertext (because each ciphertext has its own
cryptographic integrity check), but Eve *will* be able to swap ciphertexts for
one another, and revert ciphertexts to what they used to be at previous times.
If we needed to defend against such attacks, we would have to re-design our
threat model and come up with a different solution.
### Scenario #2: Encrypting account data with the user's login password
This scenario is like Scenario 1, but subtly different. The threat model is as
follows. We have Alice, a server administrator, and Dave, the developer. Alice
and Dave trust each other, and Alice wants to host Dave's web application,
including its database, on her server. Alice is worried about her server getting
hacked. The application will store the users' credit card numbers, and Alice
wants to protect them in case the server gets hacked.
We can model the situation like this: after the server gets hacked, the attacker
will have read and write access to all data on it until the attack is detected
and Alice rebuilds the server. We'll call the time the attacker has access to
the server the *exposure window.* One idea to minimize loss is to encrypt the
users' credit card numbers using a key made from their login password. Then, as
long as the users all have strong passwords, and they are never logged in during
the exposure window, their credit cards will be protected from the attacker.
To implement this, Dave will use the `KeyProtectedByPassword` class. When a new
user account is created, Dave will save a new key to their account, one that's
protected by their login password:
```php
<?php
use Defuse\Crypto\KeyProtectedByPassword;
function CreateUserAccount($username, $password)
{
// ... other user account creation stuff, including password hashing
$protected_key = KeyProtectedByPassword::createRandomPasswordProtectedKey($password);
$protected_key_encoded = $protected_key->saveToAsciiSafeString();
// ... save $protected_key_encoded into the user's account record
}
```
**WARNING:** Because of the way `KeyProtectedByPassword` is implemented, knowing
`SHA256($password)` is enough to decrypt a `KeyProtectedByPassword`. To be
secure, your application MUST NOT EVER compute `SHA256($password)` and use or
store it for any reason. You must also make sure that other libraries your
application is using don't compute it either.
Then, when the user logs in, Dave's code will load the protected key from the
user's account record, unlock it to get a `Key` object, and save the `Key`
object somewhere safe (like temporary memory-backed session storage or
a cookie). Note that wherever Dave's code saves the key, it must be destroyed
once the user logs out, or else the attacker might be able to find users' keys
even if they were never logged in during the attack.
```php
<?php
use Defuse\Crypto\KeyProtectedByPassword;
// ... authenticate the user using a good password hashing scheme
// keep the user's password in $password
$protected_key_encoded = // ... load it from the user's account record
$protected_key = KeyProtectedByPassword::loadFromAsciiSafeString($protected_key_encoded);
$user_key = $protected_key->unlockKey($password);
$user_key_encoded = $user_key->saveToAsciiSafeString();
// ... save $user_key_encoded in a cookie
```
```php
<?php
// ... when the user is logging out ...
// ... securely wipe the saved $user_key_encoded from the system ...
```
When a user adds their credit card number, Dave's code will get the key from the
memory-backed session or cookie and use it to encrypt the credit card number:
```php
<?php
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
// ...
$user_key_encoded = // ... get it out of the cookie ...
$user_key = Key::loadFromAsciiSafeString($user_key_encoded);
// ...
$credit_card_number = // ... get credit card number from the user
$encrypted_card_number = Crypto::encrypt($credit_card_number, $user_key);
// ... save $encrypted_card_number in the database
```
When the application needs to use the credit card number, it will decrypt it:
```php
<?php
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
// ...
$user_key_encoded = // ... get it out of the cookie
$user_key = Key::loadFromAsciiSafeString($user_key_encoded);
// ...
$encrypted_card_number = // ... load it from the database ...
try {
$credit_card_number = Crypto::decrypt($encrypted_card_number, $user_key);
} catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
// Either there's a bug in our code, we're trying to decrypt with the
// wrong key, or the encrypted credit card number was corrupted in the
// database.
// ... handle this case ...
}
```
With all caveats carefully heeded, this solution limits credit card number
exposure in the case where Alice's server gets hacked for a short amount of
time. Remember to think about the attacks that *aren't* included in our threat
model. The attacker is still free to do all sorts of harmful things like
modifying the server's data which may go undetected if Alice doesn't have secure
backups to compare against.
Getting Help
-------------
If you're having difficulty using the library, see if your problem is already
solved by an answer in the [FAQ](FAQ.md).

View File

@ -0,0 +1,51 @@
Upgrading From Version 1.2
===========================
With version 2.0.0 of this library came major changes to the ciphertext format,
algorithms used for encryption, and API.
In version 1.2, keys were represented by 16-byte string variables. In version
2.0.0, keys are represented by objects, instances of the `Key` class. This
change was made in order to make it harder to misuse the API. For example, in
version 1.2, you could pass in *any* 16-byte string, but in version 2.0.0 you
need a `Key` object, which you can only get if you're "doing the right thing."
This means that for all of your old version 1.2 keys, you'll have to:
1. Generate a new version 2.0.0 key.
2. For all of the ciphertexts encrypted under the old key:
1. Decrypt the ciphertext using the old version 1.2 key.
2. Re-encrypt it using the new version 2.0.0 key.
Use the special `Crypto::legacyDecrypt()` method to decrypt the old ciphertexts
using the old key and then re-encrypt them using `Crypto::encrypt()` with the
new key. Your code will look something like the following. To avoid data loss,
securely back up your keys and data before running your upgrade code.
```php
<?php
// ...
$legacy_ciphertext = // ... get the ciphertext you want to upgrade ...
$legacy_key = // ... get the key to decrypt this ciphertext ...
// Generate the new key that we'll re-encrypt the ciphertext with.
$new_key = Key::createNewRandomKey();
// ... save it somewhere ...
// Decrypt it.
try {
$plaintext = Crypto::legacyDecrypt($legacy_ciphertext, $legacy_key);
} catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex)
{
// ... TODO: handle this case appropriately ...
}
// Re-encrypt it.
$new_ciphertext = Crypto::encrypt($plaintext, $new_key);
// ... replace the old $legacy_ciphertext with the new $new_ciphertext
// ...
```

View File

@ -0,0 +1,280 @@
Class: Defuse\Crypto\Crypto
============================
The `Crypto` class provides encryption and decryption of strings either using
a secret key or secret password. For encryption and decryption of large files,
see the `File` class.
This code for this class is in `src/Crypto.php`.
Instance Methods
-----------------
This class has no instance methods.
Static Methods
---------------
### Crypto::encrypt($plaintext, Key $key, $raw\_binary = false)
**Description:**
Encrypts a plaintext string using a secret key.
**Parameters:**
1. `$plaintext` is the string to encrypt.
2. `$key` is an instance of `Key` containing the secret key for encryption.
3. `$raw_binary` determines whether the output will be a byte string (true) or
hex encoded (false, the default).
**Return value:**
Returns a ciphertext string representing `$plaintext` encrypted with the key
`$key`. Knowledge of `$key` is required in order to decrypt the ciphertext and
recover the plaintext.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `\TypeError` is thrown if the parameters are not of the expected types.
**Side-effects and performance:**
This method runs a small and very fast set of self-tests if it is the very first
time one of the `Crypto` methods has been called. The performance overhead is
negligible and can be safely ignored in all applications.
**Cautions:**
The ciphertext returned by this method is decryptable by anyone with knowledge
of the key `$key`. It is the caller's responsibility to keep `$key` secret.
Where `$key` should be stored is up to the caller and depends on the threat
model the caller is designing their application under. If you are unsure where
to store `$key`, consult with a professional cryptographer to get help designing
your application.
Please note that **encryption does not, and is not intended to, hide the
*length* of the data being encrypted.** For example, it is not safe to encrypt
a field in which only a small number of different-length values are possible
(e.g. "male" or "female") since it would be possible to tell what the plaintext
is by looking at the length of the ciphertext. In order to do this safely, it is
your responsibility to, before encrypting, pad the data out to the length of the
longest string that will ever be encrypted. This way, all plaintexts are the
same length, and no information about the plaintext can be gleaned from the
length of the ciphertext.
### Crypto::decrypt($ciphertext, Key $key, $raw\_binary = false)
**Description:**
Decrypts a ciphertext string using a secret key.
**Parameters:**
1. `$ciphertext` is the ciphertext to be decrypted.
2. `$key` is an instance of `Key` containing the secret key for decryption.
3. `$raw_binary` must have the same value as the `$raw_binary` given to the
call to `encrypt()` that generated `$ciphertext`.
**Return value:**
If the decryption succeeds, returns a string containing the same value as the
string that was passed to `encrypt()` when `$ciphertext` was produced. Upon
a successful return, the caller can be assured that `$ciphertext` could not have
been produced except by someone with knowledge of `$key`.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
the `$key` is not the correct key for the given ciphertext, or if the
ciphertext has been modified (possibly maliciously). There is no way to
distinguish between these two cases.
- `\TypeError` is thrown if the parameters are not of the expected types.
**Side-effects and performance:**
This method runs a small and very fast set of self-tests if it is the very first
time one of the `Crypto` methods has been called. The performance overhead is
negligible and can be safely ignored in all applications.
**Cautions:**
It is impossible in principle to distinguish between the case where you attempt
to decrypt with the wrong key and the case where you attempt to decrypt
a modified (corrupted) ciphertext. It is up to the caller how to best deal with
this ambiguity, as it depends on the application this library is being used in.
If in doubt, consult with a professional cryptographer.
### Crypto::encryptWithPassword($plaintext, $password, $raw\_binary = false)
**Description:**
Encrypts a plaintext string using a secret password.
**Parameters:**
1. `$plaintext` is the string to encrypt.
2. `$password` is a string containing the secret password used for encryption.
3. `$raw_binary` determines whether the output will be a byte string (true) or
hex encoded (false, the default).
**Return value:**
Returns a ciphertext string representing `$plaintext` encrypted with a key
derived from `$password`. Knowledge of `$password` is required in order to
decrypt the ciphertext and recover the plaintext.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `\TypeError` is thrown if the parameters are not of the expected types.
**Side-effects and performance:**
This method is intentionally slow, using a lot of CPU resources for a fraction
of a second. It applies key stretching to the password in order to make password
guessing attacks more computationally expensive. If you need a faster way to
encrypt multiple ciphertexts under the same password, see the
`KeyProtectedByPassword` class.
This method runs a small and very fast set of self-tests if it is the very first
time one of the `Crypto` methods has been called. The performance overhead is
negligible and can be safely ignored in all applications.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
### Crypto::decryptWithPassword($ciphertext, $password, $raw\_binary = false)
**Description:**
Decrypts a ciphertext string using a secret password.
**Parameters:**
1. `$ciphertext` is the ciphertext to be decrypted.
2. `$password` is a string containing the secret password used for decryption.
3. `$raw_binary` must have the same value as the `$raw_binary` given to the
call to `encryptWithPassword()` that generated `$ciphertext`.
**Return value:**
If the decryption succeeds, returns a string containing the same value as the
string that was passed to `encryptWithPassword()` when `$ciphertext` was
produced. Upon a successful return, the caller can be assured that `$ciphertext`
could not have been produced except by someone with knowledge of `$password`.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
the `$password` is not the correct password for the given ciphertext, or if
the ciphertext has been modified (possibly maliciously). There is no way to
distinguish between these two cases.
- `\TypeError` is thrown if the parameters are not of the expected types.
**Side-effects:**
This method is intentionally slow. It applies key stretching to the password in
order to make password guessing attacks more computationally expensive. If you
need a faster way to encrypt multiple ciphertexts under the same password, see
the `KeyProtectedByPassword` class.
This method runs a small and very fast set of self-tests if it is the very first
time one of the `Crypto` methods has been called. The performance overhead is
negligible and can be safely ignored in all applications.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
It is impossible in principle to distinguish between the case where you attempt
to decrypt with the wrong password and the case where you attempt to decrypt
a modified (corrupted) ciphertext. It is up to the caller how to best deal with
this ambiguity, as it depends on the application this library is being used in.
If in doubt, consult with a professional cryptographer.
### Crypto::legacyDecrypt($ciphertext, $key)
**Description:**
Decrypts a ciphertext produced by version 1 of this library so that the
plaintext can be re-encrypted into a version 2 ciphertext. See [Upgrading from
v1.2](../UpgradingFromV1.2.md).
**Parameters:**
1. `$ciphertext` is a ciphertext produced by version 1.x of this library.
2. `$key` is a 16-byte string (*not* a Key object) containing the key that was
used with version 1.x of this library to produce `$ciphertext`.
**Return value:**
If the decryption succeeds, returns the string that was encrypted to make
`$ciphertext` by version 1.x of this library. Upon a successful return, the
caller can be assured that `$ciphertext` could not have been produced except by
someone with knowledge of `$key`.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
the `$key` is not the correct key for the given ciphertext, or if the
ciphertext has been modified (possibly maliciously). There is no way to
distinguish between these two cases.
- `\TypeError` is thrown if the parameters are not of the expected types.
**Side-effects:**
This method runs a small and very fast set of self-tests if it is the very first
time one of the `Crypto` methods has been called. The performance overhead is
negligible and can be safely ignored in all applications.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$key` may be leaked out to an attacker through the stack trace. We
recommend configuring PHP to never output stack traces (either displaying them
to the user or saving them to log files).
It is impossible in principle to distinguish between the case where you attempt
to decrypt with the wrong key and the case where you attempt to decrypt
a modified (corrupted) ciphertext. It is up to the caller how to best deal with
this ambiguity, as it depends on the application this library is being used in.
If in doubt, consult with a professional cryptographer.

View File

@ -0,0 +1,486 @@
Class: Defuse\Crypto\File
==========================
Instance Methods
-----------------
This class has no instance methods.
Static Methods
---------------
### File::encryptFile($inputFilename, $outputFilename, Key $key)
**Description:**
Encrypts a file using a secret key.
**Parameters:**
1. `$inputFilename` is the path to a file containing the plaintext to encrypt.
2. `$outputFilename` is the path to save the ciphertext file.
3. `$key` is an instance of `Key` containing the secret key for encryption.
**Behavior:**
Encrypts the contents of the input file, writing the result to the output file.
If the output file already exists, it is overwritten.
**Return value:**
Does not return a value.
**Exceptions:**
- `Defuse\Crypto\Exception\IOException` is thrown if there is an I/O error.
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
**Side-effects and performance:**
None.
**Cautions:**
The ciphertext output by this method is decryptable by anyone with knowledge of
the key `$key`. It is the caller's responsibility to keep `$key` secret. Where
`$key` should be stored is up to the caller and depends on the threat model the
caller is designing their application under. If you are unsure where to store
`$key`, consult with a professional cryptographer to get help designing your
application.
Please note that **encryption does not, and is not intended to, hide the
*length* of the data being encrypted.** For example, it is not safe to encrypt
a field in which only a small number of different-length values are possible
(e.g. "male" or "female") since it would be possible to tell what the plaintext
is by looking at the length of the ciphertext. In order to do this safely, it is
your responsibility to, before encrypting, pad the data out to the length of the
longest string that will ever be encrypted. This way, all plaintexts are the
same length, and no information about the plaintext can be gleaned from the
length of the ciphertext.
### File::decryptFile($inputFilename, $outputFilename, Key $key)
**Description:**
Decrypts a file using a secret key.
**Parameters:**
1. `$inputFilename` is the path to a file containing the ciphertext to decrypt.
2. `$outputFilename` is the path to save the decrypted plaintext file.
3. `$key` is an instance of `Key` containing the secret key for decryption.
**Behavior:**
Decrypts the contents of the input file, writing the result to the output file.
If the output file already exists, it is overwritten.
**Return value:**
Does not return a value.
**Exceptions:**
- `Defuse\Crypto\Exception\IOException` is thrown if there is an I/O error.
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
the `$key` is not the correct key for the given ciphertext, or if the
ciphertext has been modified (possibly maliciously). There is no way to
distinguish between these two cases.
**Side-effects and performance:**
The input ciphertext is processed in two passes. The first pass verifies the
integrity and the second pass performs the actual decryption of the file and
writing to the output file. This is done in a streaming manner so that only
a small part of the file is ever loaded into memory at a time.
**Cautions:**
Be aware that when `Defuse\Crypto\WrongKeyOrModifiedCiphertextException` is
thrown, some partial plaintext data may have been written to the output. Any
plaintext data that is output is guaranteed to be a prefix of the original
plaintext (i.e. at worst it was truncated). This can only happen if an attacker
modifies the input between the first pass (integrity check) and the second pass
(decryption) over the file.
It is impossible in principle to distinguish between the case where you attempt
to decrypt with the wrong key and the case where you attempt to decrypt
a modified (corrupted) ciphertext. It is up to the caller how to best deal with
this ambiguity, as it depends on the application this library is being used in.
If in doubt, consult with a professional cryptographer.
### File::encryptFileWithPassword($inputFilename, $outputFilename, $password)
**Description:**
Encrypts a file with a password.
**Parameters:**
1. `$inputFilename` is the path to a file containing the plaintext to encrypt.
2. `$outputFilename` is the path to save the ciphertext file.
3. `$password` is the password used for decryption.
**Behavior:**
Encrypts the contents of the input file, writing the result to the output file.
If the output file already exists, it is overwritten.
**Return value:**
Does not return a value.
**Exceptions:**
- `Defuse\Crypto\Exception\IOException` is thrown if there is an I/O error.
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
**Side-effects and performance:**
This method is intentionally slow, using a lot of CPU resources for a fraction
of a second. It applies key stretching to the password in order to make password
guessing attacks more computationally expensive. If you need a faster way to
encrypt multiple ciphertexts under the same password, see the
`KeyProtectedByPassword` class.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
Please note that **encryption does not, and is not intended to, hide the
*length* of the data being encrypted.** For example, it is not safe to encrypt
a field in which only a small number of different-length values are possible
(e.g. "male" or "female") since it would be possible to tell what the plaintext
is by looking at the length of the ciphertext. In order to do this safely, it is
your responsibility to, before encrypting, pad the data out to the length of the
longest string that will ever be encrypted. This way, all plaintexts are the
same length, and no information about the plaintext can be gleaned from the
length of the ciphertext.
### File::decryptFileWithPassword($inputFilename, $outputFilename, $password)
**Description:**
Decrypts a file with a password.
**Parameters:**
1. `$inputFilename` is the path to a file containing the ciphertext to decrypt.
2. `$outputFilename` is the path to save the decrypted plaintext file.
3. `$password` is the password used for decryption.
**Behavior:**
Decrypts the contents of the input file, writing the result to the output file.
If the output file already exists, it is overwritten.
**Return value:**
Does not return a value.
**Exceptions:**
- `Defuse\Crypto\Exception\IOException` is thrown if there is an I/O error.
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
the `$password` is not the correct key for the given ciphertext, or if the
ciphertext has been modified (possibly maliciously). There is no way to
distinguish between these two cases.
**Side-effects and performance:**
This method is intentionally slow, using a lot of CPU resources for a fraction
of a second. It applies key stretching to the password in order to make password
guessing attacks more computationally expensive. If you need a faster way to
encrypt multiple ciphertexts under the same password, see the
`KeyProtectedByPassword` class.
The input ciphertext is processed in two passes. The first pass verifies the
integrity and the second pass performs the actual decryption of the file and
writing to the output file. This is done in a streaming manner so that only
a small part of the file is ever loaded into memory at a time.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
Be aware that when `Defuse\Crypto\WrongKeyOrModifiedCiphertextException` is
thrown, some partial plaintext data may have been written to the output. Any
plaintext data that is output is guaranteed to be a prefix of the original
plaintext (i.e. at worst it was truncated). This can only happen if an attacker
modifies the input between the first pass (integrity check) and the second pass
(decryption) over the file.
It is impossible in principle to distinguish between the case where you attempt
to decrypt with the wrong password and the case where you attempt to decrypt
a modified (corrupted) ciphertext. It is up to the caller how to best deal with
this ambiguity, as it depends on the application this library is being used in.
If in doubt, consult with a professional cryptographer.
### File::encryptResource($inputHandle, $outputHandle, Key $key)
**Description:**
Encrypts a resource (stream) with a secret key.
**Parameters:**
1. `$inputHandle` is a handle to a resource (like a file pointer) containing the
plaintext to encrypt.
2. `$outputHandle` is a handle to a resource (like a file pointer) that the
ciphertext will be written to.
3. `$key` is an instance of `Key` containing the secret key for encryption.
**Behavior:**
Encrypts the data read from the input stream and writes it to the output stream.
**Return value:**
Does not return a value.
**Exceptions:**
- `Defuse\Crypto\Exception\IOException` is thrown if there is an I/O error.
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
**Side-effects and performance:**
None.
**Cautions:**
The ciphertext output by this method is decryptable by anyone with knowledge of
the key `$key`. It is the caller's responsibility to keep `$key` secret. Where
`$key` should be stored is up to the caller and depends on the threat model the
caller is designing their application under. If you are unsure where to store
`$key`, consult with a professional cryptographer to get help designing your
application.
Please note that **encryption does not, and is not intended to, hide the
*length* of the data being encrypted.** For example, it is not safe to encrypt
a field in which only a small number of different-length values are possible
(e.g. "male" or "female") since it would be possible to tell what the plaintext
is by looking at the length of the ciphertext. In order to do this safely, it is
your responsibility to, before encrypting, pad the data out to the length of the
longest string that will ever be encrypted. This way, all plaintexts are the
same length, and no information about the plaintext can be gleaned from the
length of the ciphertext.
### File::decryptResource($inputHandle, $outputHandle, Key $key)
**Description:**
Decrypts a resource (stream) with a secret key.
**Parameters:**
1. `$inputHandle` is a handle to a file-backed resource containing the
ciphertext to decrypt. It must be a file not a network stream or standard
input.
2. `$outputHandle` is a handle to a resource (like a file pointer) that the
plaintext will be written to.
3. `$key` is an instance of `Key` containing the secret key for decryption.
**Behavior:**
Decrypts the data read from the input stream and writes it to the output stream.
**Return value:**
Does not return a value.
**Exceptions:**
- `Defuse\Crypto\Exception\IOException` is thrown if there is an I/O error.
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
the `$key` is not the correct key for the given ciphertext, or if the
ciphertext has been modified (possibly maliciously). There is no way to
distinguish between these two cases.
**Side-effects and performance:**
The input ciphertext is processed in two passes. The first pass verifies the
integrity and the second pass performs the actual decryption of the file and
writing to the output file. This is done in a streaming manner so that only
a small part of the file is ever loaded into memory at a time.
**Cautions:**
Be aware that when `Defuse\Crypto\WrongKeyOrModifiedCiphertextException` is
thrown, some partial plaintext data may have been written to the output. Any
plaintext data that is output is guaranteed to be a prefix of the original
plaintext (i.e. at worst it was truncated). This can only happen if an attacker
modifies the input between the first pass (integrity check) and the second pass
(decryption) over the file.
It is impossible in principle to distinguish between the case where you attempt
to decrypt with the wrong key and the case where you attempt to decrypt
a modified (corrupted) ciphertext. It is up to the caller how to best deal with
this ambiguity, as it depends on the application this library is being used in.
If in doubt, consult with a professional cryptographer.
### File::encryptResourceWithPassword($inputHandle, $outputHandle, $password)
**Description:**
Encrypts a resource (stream) with a password.
**Parameters:**
1. `$inputHandle` is a handle to a resource (like a file pointer) containing the
plaintext to encrypt.
2. `$outputHandle` is a handle to a resource (like a file pointer) that the
ciphertext will be written to.
3. `$password` is the password used for encryption.
**Behavior:**
Encrypts the data read from the input stream and writes it to the output stream.
**Return value:**
Does not return a value.
**Exceptions:**
- `Defuse\Crypto\Exception\IOException` is thrown if there is an I/O error.
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
**Side-effects and performance:**
This method is intentionally slow, using a lot of CPU resources for a fraction
of a second. It applies key stretching to the password in order to make password
guessing attacks more computationally expensive. If you need a faster way to
encrypt multiple ciphertexts under the same password, see the
`KeyProtectedByPassword` class.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
Please note that **encryption does not, and is not intended to, hide the
*length* of the data being encrypted.** For example, it is not safe to encrypt
a field in which only a small number of different-length values are possible
(e.g. "male" or "female") since it would be possible to tell what the plaintext
is by looking at the length of the ciphertext. In order to do this safely, it is
your responsibility to, before encrypting, pad the data out to the length of the
longest string that will ever be encrypted. This way, all plaintexts are the
same length, and no information about the plaintext can be gleaned from the
length of the ciphertext.
### File::decryptResourceWithPassword($inputHandle, $outputHandle, $password)
**Description:**
Decrypts a resource (stream) with a password.
**Parameters:**
1. `$inputHandle` is a handle to a file-backed resource containing the
ciphertext to decrypt. It must be a file not a network stream or standard
input.
2. `$outputHandle` is a handle to a resource (like a file pointer) that the
plaintext will be written to.
3. `$password` is the password used for decryption.
**Behavior:**
Decrypts the data read from the input stream and writes it to the output stream.
**Return value:**
Does not return a value.
**Exceptions:**
- `Defuse\Crypto\Exception\IOException` is thrown if there is an I/O error.
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
the `$password` is not the correct key for the given ciphertext, or if the
ciphertext has been modified (possibly maliciously). There is no way to
distinguish between these two cases.
**Side-effects and performance:**
This method is intentionally slow, using a lot of CPU resources for a fraction
of a second. It applies key stretching to the password in order to make password
guessing attacks more computationally expensive. If you need a faster way to
encrypt multiple ciphertexts under the same password, see the
`KeyProtectedByPassword` class.
The input ciphertext is processed in two passes. The first pass verifies the
integrity and the second pass performs the actual decryption of the file and
writing to the output file. This is done in a streaming manner so that only
a small part of the file is ever loaded into memory at a time.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
Be aware that when `Defuse\Crypto\WrongKeyOrModifiedCiphertextException` is
thrown, some partial plaintext data may have been written to the output. Any
plaintext data that is output is guaranteed to be a prefix of the original
plaintext (i.e. at worst it was truncated). This can only happen if an attacker
modifies the input between the first pass (integrity check) and the second pass
(decryption) over the file.
It is impossible in principle to distinguish between the case where you attempt
to decrypt with the wrong password and the case where you attempt to decrypt
a modified (corrupted) ciphertext. It is up to the caller how to best deal with
this ambiguity, as it depends on the application this library is being used in.
If in doubt, consult with a professional cryptographer.

View File

@ -0,0 +1,117 @@
Class: Defuse\Crypto\Key
=========================
The `Key` class represents a secret key used for encrypting and decrypting. Once
you have a `Key` instance, you can use it with the `Crypto` class to encrypt and
decrypt strings and with the `File` class to encrypt and decrypt files.
Instance Methods
-----------------
### saveToAsciiSafeString()
**Description:**
Saves the encryption key to a string of printable ASCII characters, which can be
loaded again into a `Key` instance using `Key::loadFromAsciiSafeString()`.
**Parameters:**
This method does not take any parameters.
**Return value:**
Returns a string of printable ASCII characters representing this `Key` instance,
which can be loaded back into an instance of `Key` using
`Key::loadFromAsciiSafeString()`.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
**Side-effects and performance:**
None.
**Cautions:**
This method currently returns a hexadecimal string. You should not rely on this
behavior. For example, it may be improved in the future to return a base64
string.
Static Methods
---------------
### Key::createNewRandomKey()
**Description:**
Generates a new random key and returns an instance of `Key`.
**Parameters:**
This method does not take any parameters.
**Return value:**
Returns an instance of `Key` containing a randomly-generated encryption key.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
**Side-effects and performance:**
None.
**Cautions:**
None.
### Key::loadFromAsciiSafeString($saved\_key\_string, $do\_not\_trim = false)
**Description:**
Loads an instance of `Key` that was saved to a string by
`saveToAsciiSafeString()`.
By default, this function will call `Encoding::trimTrailingWhitespace()`
to remove trailing CR, LF, NUL, TAB, and SPACE characters, which are commonly
appended to files when working with text editors.
**Parameters:**
1. `$saved_key_string` is the string returned from `saveToAsciiSafeString()`
when the original `Key` instance was saved.
2. `$do_not_trim` should be set to `TRUE` if you do not wish for the library
to automatically strip trailing whitespace from the string.
**Return value:**
Returns an instance of `Key` representing the same encryption key as the one
that was represented by the `Key` instance that got saved into
`$saved_key_string` by a call to `saveToAsciiSafeString()`.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\BadFormatException` is thrown whenever
`$saved_key_string` does not represent a valid `Key` instance.
**Side-effects and performance:**
None.
**Cautions:**
None.

View File

@ -0,0 +1,259 @@
Class: Defuse\Crypto\KeyProtectedByPassword
============================================
The `KeyProtectedByPassword` class represents a key that is "locked" with
a password. In order to obtain an instance of `Key` that you can use for
encrypting and decrypting, a `KeyProtectedByPassword` must first be "unlocked"
by providing the correct password.
`KeyProtectedByPassword` provides an alternative to using the
`encryptWithPassword()`, `decryptWithPassword()`, `encryptFileWithPassword()`,
and `decryptFileWithPassword()` methods with several advantages:
- The slow and computationally-expensive key stretching is run only once when
you unlock a `KeyProtectedByPassword` to obtain the `Key`.
- You do not have to keep the original password in memory to encrypt and decrypt
things. After you've obtained the `Key` from a `KeyProtectedByPassword`, the
password is no longer necessary.
Instance Methods
-----------------
### saveToAsciiSafeString()
**Description:**
Saves the protected key to a string of printable ASCII characters, which can be
loaded again into a `KeyProtectedByPassword` instance using
`KeyProtectedByPassword::loadFromAsciiSafeString()`.
**Parameters:**
This method does not take any parameters.
**Return value:**
Returns a string of printable ASCII characters representing this
`KeyProtectedByPassword` instance, which can be loaded back into an instance of
`KeyProtectedByPassword` using
`KeyProtectedByPassword::loadFromAsciiSafeString()`.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
**Side-effects and performance:**
None.
**Cautions:**
This method currently returns a hexadecimal string. You should not rely on this
behavior. For example, it may be improved in the future to return a base64
string.
### unlockKey($password)
**Description:**
Unlocks the password-protected key, obtaining a `Key` which can be used for
encryption and decryption.
**Parameters:**
1. `$password` is the password required to unlock this `KeyProtectedByPassword`
to obtain the `Key`.
**Return value:**
If `$password` is the correct password, then this method returns an instance of
the `Key` class.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
either the given `$password` is not the correct password for this
`KeyProtectedByPassword` or the ciphertext stored internally by this object
has been modified, i.e. it was accidentally corrupted or intentionally
corrupted by an attacker. There is no way for the caller to distinguish
between these two cases.
**Side-effects and performance:**
This method runs a small and very fast set of self-tests if it is the very first
time this method or one of the `Crypto` methods has been called. The performance
overhead is negligible and can be safely ignored in all applications.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
It is impossible in principle to distinguish between the case where you attempt
to unlock with the wrong password and the case where you attempt to unlock
a modified (corrupted) `KeyProtectedByPassword`. It is up to the caller how to
best deal with this ambiguity, as it depends on the application this library is
being used in. If in doubt, consult with a professional cryptographer.
### changePassword($current\_password, $new\_password)
**Description:**
Changes the password, so that calling `unlockKey` on this object in the future
will require you to pass `$new\_password` instead of the old password. It is
your responsibility to overwrite all stored copies of this
`KeyProtectedByPassword`. Any copies you leave lying around can still be
decrypted with the old password.
**Parameters:**
1. `$current\_password` is the password that this `KeyProtectedByPassword` is
currently protected with.
2. `$new\_password` is the new password, which the `KeyProtectedByPassword` will
be protected with once this operation completes.
**Return value:**
If `$current\_password` is the correct password, then this method updates itself
to be protected with the new password, and also returns itself.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException` is thrown if
either the given `$current\_password` is not the correct password for this
`KeyProtectedByPassword` or the ciphertext stored internally by this object
has been modified, i.e. it was accidentally corrupted or intentionally
corrupted by an attacker. There is no way for the caller to distinguish
between these two cases.
**Side-effects and performance:**
This method runs a small and very fast set of self-tests if it is the very first
time this method or one of the `Crypto` methods has been called. The performance
overhead is negligible and can be safely ignored in all applications.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
It is impossible in principle to distinguish between the case where you attempt
to unlock with the wrong password and the case where you attempt to unlock
a modified (corrupted) `KeyProtectedByPassword`. It is up to the caller how to
best deal with this ambiguity, as it depends on the application this library is
being used in. If in doubt, consult with a professional cryptographer.
**WARNING:** Because of the way `KeyProtectedByPassword` is implemented, knowing
`SHA256($password)` is enough to decrypt a `KeyProtectedByPassword`. To be
secure, your application MUST NOT EVER compute `SHA256($password)` and use or
store it for any reason. You must also make sure that other libraries your
application is using don't compute it either.
Static Methods
---------------
### KeyProtectedByPassword::createRandomPasswordProtectedKey($password)
**Description:**
Generates a new random key that's protected by the string `$password` and
returns an instance of `KeyProtectedByPassword`.
**Parameters:**
1. `$password` is the password used to protect the random key.
**Return value:**
Returns an instance of `KeyProtectedByPassword` containing a randomly-generated
encryption key that's protected by the password `$password`.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
**Side-effects and performance:**
This method runs a small and very fast set of self-tests if it is the very first
time this method or one of the `Crypto` methods has been called. The performance
overhead is negligible and can be safely ignored in all applications.
**Cautions:**
PHP stack traces display (portions of) the arguments passed to methods on the
call stack. If an exception is thrown inside this call, and it is uncaught, the
value of `$password` may be leaked out to an attacker through the stack trace.
We recommend configuring PHP to never output stack traces (either displaying
them to the user or saving them to log files).
Be aware that if you protecting multiple keys with the same password, an
attacker with write access to your system will be able to swap the protected
keys around so that the wrong key gets used next time it is unlocked. This could
lead to data being encrypted with the wrong key, perhaps one that the attacker
knows.
**WARNING:** Because of the way `KeyProtectedByPassword` is implemented, knowing
`SHA256($password)` is enough to decrypt a `KeyProtectedByPassword`. To be
secure, your application MUST NOT EVER compute `SHA256($password)` and use or
store it for any reason. You must also make sure that other libraries your
application is using don't compute it either.
### KeyProtectedByPassword::loadFromAsciiSafeString($saved\_key\_string)
**Description:**
Loads an instance of `KeyProtectedByPassword` that was saved to a string by
`saveToAsciiSafeString()`.
**Parameters:**
1. `$saved_key_string` is the string returned from `saveToAsciiSafeString()`
when the original `KeyProtectedByPassword` instance was saved.
**Return value:**
Returns an instance of `KeyProtectedByPassword` representing the same
password-protected key as the one that was represented by the
`KeyProtectedByPassword` instance that got saved into `$saved_key_string` by
a call to `saveToAsciiSafeString()`.
**Exceptions:**
- `Defuse\Crypto\Exception\EnvironmentIsBrokenException` is thrown either when
the platform the code is running on cannot safely perform encryption for some
reason (e.g. it lacks a secure random number generator), or the runtime tests
detected a bug in this library.
- `Defuse\Crypto\Exception\BadFormatException` is thrown whenever
`$saved_key_string` does not represent a valid `KeyProtectedByPassword`
instance.
**Side-effects and performance:**
None.
**Cautions:**
None.

View File

@ -0,0 +1,470 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Core
{
const HEADER_VERSION_SIZE = 4;
const MINIMUM_CIPHERTEXT_SIZE = 84;
const CURRENT_VERSION = "\xDE\xF5\x02\x00";
const CIPHER_METHOD = 'aes-256-ctr';
const BLOCK_BYTE_SIZE = 16;
const KEY_BYTE_SIZE = 32;
const SALT_BYTE_SIZE = 32;
const MAC_BYTE_SIZE = 32;
const HASH_FUNCTION_NAME = 'sha256';
const ENCRYPTION_INFO_STRING = 'DefusePHP|V2|KeyForEncryption';
const AUTHENTICATION_INFO_STRING = 'DefusePHP|V2|KeyForAuthentication';
const BUFFER_BYTE_SIZE = 1048576;
const LEGACY_CIPHER_METHOD = 'aes-128-cbc';
const LEGACY_BLOCK_BYTE_SIZE = 16;
const LEGACY_KEY_BYTE_SIZE = 16;
const LEGACY_HASH_FUNCTION_NAME = 'sha256';
const LEGACY_MAC_BYTE_SIZE = 32;
const LEGACY_ENCRYPTION_INFO_STRING = 'DefusePHP|KeyForEncryption';
const LEGACY_AUTHENTICATION_INFO_STRING = 'DefusePHP|KeyForAuthentication';
/*
* V2.0 Format: VERSION (4 bytes) || SALT (32 bytes) || IV (16 bytes) ||
* CIPHERTEXT (varies) || HMAC (32 bytes)
*
* V1.0 Format: HMAC (32 bytes) || IV (16 bytes) || CIPHERTEXT (varies).
*/
/**
* Adds an integer to a block-sized counter.
*
* @param string $ctr
* @param int $inc
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*
* @psalm-suppress RedundantCondition - It's valid to use is_int to check for overflow.
*/
public static function incrementCounter($ctr, $inc)
{
Core::ensureTrue(
Core::ourStrlen($ctr) === Core::BLOCK_BYTE_SIZE,
'Trying to increment a nonce of the wrong size.'
);
Core::ensureTrue(
\is_int($inc),
'Trying to increment nonce by a non-integer.'
);
// The caller is probably re-using CTR-mode keystream if they increment by 0.
Core::ensureTrue(
$inc > 0,
'Trying to increment a nonce by a nonpositive amount'
);
Core::ensureTrue(
$inc <= PHP_INT_MAX - 255,
'Integer overflow may occur'
);
/*
* We start at the rightmost byte (big-endian)
* So, too, does OpenSSL: http://stackoverflow.com/a/3146214/2224584
*/
for ($i = Core::BLOCK_BYTE_SIZE - 1; $i >= 0; --$i) {
$sum = \ord($ctr[$i]) + $inc;
/* Detect integer overflow and fail. */
Core::ensureTrue(\is_int($sum), 'Integer overflow in CTR mode nonce increment');
$ctr[$i] = \pack('C', $sum & 0xFF);
$inc = $sum >> 8;
}
return $ctr;
}
/**
* Returns a random byte string of the specified length.
*
* @param int $octets
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function secureRandom($octets)
{
if ($octets <= 0) {
throw new Ex\CryptoException(
'A zero or negative amount of random bytes was requested.'
);
}
self::ensureFunctionExists('random_bytes');
try {
return \random_bytes(max(1, $octets));
} catch (\Exception $ex) {
throw new Ex\EnvironmentIsBrokenException(
'Your system does not have a secure random number generator.'
);
}
}
/**
* Computes the HKDF key derivation function specified in
* http://tools.ietf.org/html/rfc5869.
*
* @param string $hash Hash Function
* @param string $ikm Initial Keying Material
* @param int $length How many bytes?
* @param string $info What sort of key are we deriving?
* @param string $salt
*
* @throws Ex\EnvironmentIsBrokenException
* @psalm-suppress UndefinedFunction - We're checking if the function exists first.
*
* @return string
*/
public static function HKDF($hash, $ikm, $length, $info = '', $salt = null)
{
static $nativeHKDF = null;
if ($nativeHKDF === null) {
$nativeHKDF = \is_callable('\\hash_hkdf');
}
if ($nativeHKDF) {
if (\is_null($salt)) {
$salt = '';
}
return \hash_hkdf($hash, $ikm, $length, $info, $salt);
}
$digest_length = Core::ourStrlen(\hash_hmac($hash, '', '', true));
// Sanity-check the desired output length.
Core::ensureTrue(
!empty($length) && \is_int($length) && $length >= 0 && $length <= 255 * $digest_length,
'Bad output length requested of HDKF.'
);
// "if [salt] not provided, is set to a string of HashLen zeroes."
if (\is_null($salt)) {
$salt = \str_repeat("\x00", $digest_length);
}
// HKDF-Extract:
// PRK = HMAC-Hash(salt, IKM)
// The salt is the HMAC key.
$prk = \hash_hmac($hash, $ikm, $salt, true);
// HKDF-Expand:
// This check is useless, but it serves as a reminder to the spec.
Core::ensureTrue(Core::ourStrlen($prk) >= $digest_length);
// T(0) = ''
$t = '';
$last_block = '';
for ($block_index = 1; Core::ourStrlen($t) < $length; ++$block_index) {
// T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??)
$last_block = \hash_hmac(
$hash,
$last_block . $info . \chr($block_index),
$prk,
true
);
// T = T(1) | T(2) | T(3) | ... | T(N)
$t .= $last_block;
}
// ORM = first L octets of T
/** @var string $orm */
$orm = Core::ourSubstr($t, 0, $length);
Core::ensureTrue(\is_string($orm));
return $orm;
}
/**
* Checks if two equal-length strings are the same without leaking
* information through side channels.
*
* @param string $expected
* @param string $given
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return bool
*/
public static function hashEquals($expected, $given)
{
static $native = null;
if ($native === null) {
$native = \function_exists('hash_equals');
}
if ($native) {
return \hash_equals($expected, $given);
}
// We can't just compare the strings with '==', since it would make
// timing attacks possible. We could use the XOR-OR constant-time
// comparison algorithm, but that may not be a reliable defense in an
// interpreted language. So we use the approach of HMACing both strings
// with a random key and comparing the HMACs.
// We're not attempting to make variable-length string comparison
// secure, as that's very difficult. Make sure the strings are the same
// length.
Core::ensureTrue(Core::ourStrlen($expected) === Core::ourStrlen($given));
$blind = Core::secureRandom(32);
$message_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $given, $blind);
$correct_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $expected, $blind);
return $correct_compare === $message_compare;
}
/**
* Throws an exception if the constant doesn't exist.
*
* @param string $name
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
*/
public static function ensureConstantExists($name)
{
Core::ensureTrue(
\defined($name),
'Constant '.$name.' does not exists'
);
}
/**
* Throws an exception if the function doesn't exist.
*
* @param string $name
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
*/
public static function ensureFunctionExists($name)
{
Core::ensureTrue(
\function_exists($name),
'function '.$name.' does not exists'
);
}
/**
* Throws an exception if the condition is false.
*
* @param bool $condition
* @param string $message
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
*/
public static function ensureTrue($condition, $message = '')
{
if (!$condition) {
throw new Ex\EnvironmentIsBrokenException($message);
}
}
/*
* We need these strlen() and substr() functions because when
* 'mbstring.func_overload' is set in php.ini, the standard strlen() and
* substr() are replaced by mb_strlen() and mb_substr().
*/
/**
* Computes the length of a string in bytes.
*
* @param string $str
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return int
*/
public static function ourStrlen($str)
{
static $exists = null;
if ($exists === null) {
$exists = \extension_loaded('mbstring') && \function_exists('mb_strlen');
}
if ($exists) {
$length = \mb_strlen($str, '8bit');
Core::ensureTrue($length !== false);
return $length;
} else {
return \strlen($str);
}
}
/**
* Behaves roughly like the function substr() in PHP 7 does.
*
* @param string $str
* @param int $start
* @param int $length
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string|bool
*/
public static function ourSubstr($str, $start, $length = null)
{
static $exists = null;
if ($exists === null) {
$exists = \extension_loaded('mbstring') && \function_exists('mb_substr');
}
// This is required to make mb_substr behavior identical to substr.
// Without this, mb_substr() would return false, contra to what the
// PHP documentation says (it doesn't say it can return false.)
$input_len = Core::ourStrlen($str);
if ($start === $input_len && !$length) {
return '';
}
if ($start > $input_len) {
return false;
}
// mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP 5.3,
// so we have to find the length ourselves. Also, substr() doesn't
// accept null for the length.
if (! isset($length)) {
if ($start >= 0) {
$length = $input_len - $start;
} else {
$length = -$start;
}
}
if ($length < 0) {
throw new \InvalidArgumentException(
"Negative lengths are not supported with ourSubstr."
);
}
if ($exists) {
$substr = \mb_substr($str, $start, $length, '8bit');
// At this point there are two cases where mb_substr can
// legitimately return an empty string. Either $length is 0, or
// $start is equal to the length of the string (both mb_substr and
// substr return an empty string when this happens). It should never
// ever return a string that's longer than $length.
if (Core::ourStrlen($substr) > $length || (Core::ourStrlen($substr) === 0 && $length !== 0 && $start !== $input_len)) {
throw new Ex\EnvironmentIsBrokenException(
'Your version of PHP has bug #66797. Its implementation of
mb_substr() is incorrect. See the details here:
https://bugs.php.net/bug.php?id=66797'
);
}
return $substr;
}
return \substr($str, $start, $length);
}
/**
* Computes the PBKDF2 password-based key derivation function.
*
* The PBKDF2 function is defined in RFC 2898. Test vectors can be found in
* RFC 6070. This implementation of PBKDF2 was originally created by Taylor
* Hornby, with improvements from http://www.variations-of-shadow.com/.
*
* @param string $algorithm The hash algorithm to use. Recommended: SHA256
* @param string $password The password.
* @param string $salt A salt that is unique to the password.
* @param int $count Iteration count. Higher is better, but slower. Recommended: At least 1000.
* @param int $key_length The length of the derived key in bytes.
* @param bool $raw_output If true, the key is returned in raw binary format. Hex encoded otherwise.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string A $key_length-byte key derived from the password and salt.
*/
public static function pbkdf2(
$algorithm,
#[\SensitiveParameter]
$password,
$salt,
$count,
$key_length,
$raw_output = false
)
{
// Type checks:
if (! \is_string($algorithm)) {
throw new \InvalidArgumentException(
'pbkdf2(): algorithm must be a string'
);
}
if (! \is_string($password)) {
throw new \InvalidArgumentException(
'pbkdf2(): password must be a string'
);
}
if (! \is_string($salt)) {
throw new \InvalidArgumentException(
'pbkdf2(): salt must be a string'
);
}
// Coerce strings to integers with no information loss or overflow
$count += 0;
$key_length += 0;
$algorithm = \strtolower($algorithm);
Core::ensureTrue(
\in_array($algorithm, \hash_algos(), true),
'Invalid or unsupported hash algorithm.'
);
// Whitelist, or we could end up with people using CRC32.
$ok_algorithms = [
'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
'ripemd160', 'ripemd256', 'ripemd320', 'whirlpool',
];
Core::ensureTrue(
\in_array($algorithm, $ok_algorithms, true),
'Algorithm is not a secure cryptographic hash function.'
);
Core::ensureTrue($count > 0 && $key_length > 0, 'Invalid PBKDF2 parameters.');
if (\function_exists('hash_pbkdf2')) {
// The output length is in NIBBLES (4-bits) if $raw_output is false!
if (! $raw_output) {
$key_length = $key_length * 2;
}
return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
}
$hash_length = Core::ourStrlen(\hash($algorithm, '', true));
$block_count = \ceil($key_length / $hash_length);
$output = '';
for ($i = 1; $i <= $block_count; $i++) {
// $i encoded as 4 bytes, big endian.
$last = $salt . \pack('N', $i);
// first iteration
$last = $xorsum = \hash_hmac($algorithm, $last, $password, true);
// perform the other $count - 1 iterations
for ($j = 1; $j < $count; $j++) {
/**
* @psalm-suppress InvalidOperand
*/
$xorsum ^= ($last = \hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if ($raw_output) {
return (string) Core::ourSubstr($output, 0, $key_length);
} else {
return Encoding::binToHex((string) Core::ourSubstr($output, 0, $key_length));
}
}
}

View File

@ -0,0 +1,477 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
class Crypto
{
/**
* Encrypts a string with a Key.
*
* @param string $plaintext
* @param Key $key
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws \TypeError
*
* @return string
*/
public static function encrypt($plaintext, $key, $raw_binary = false)
{
if (!\is_string($plaintext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($plaintext)) . ' given instead.'
);
}
if (!($key instanceof Key)) {
throw new \TypeError(
'Key expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
);
}
if (!\is_bool($raw_binary)) {
throw new \TypeError(
'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
);
}
return self::encryptInternal(
$plaintext,
KeyOrPassword::createFromKey($key),
$raw_binary
);
}
/**
* Encrypts a string with a password, using a slow key derivation function
* to make password cracking more expensive.
*
* @param string $plaintext
* @param string $password
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws \TypeError
*
* @return string
*/
public static function encryptWithPassword(
$plaintext,
#[\SensitiveParameter]
$password,
$raw_binary = false
)
{
if (!\is_string($plaintext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($plaintext)) . ' given instead.'
);
}
if (!\is_string($password)) {
throw new \TypeError(
'String expected for argument 2. ' . \ucfirst(\gettype($password)) . ' given instead.'
);
}
if (!\is_bool($raw_binary)) {
throw new \TypeError(
'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
);
}
return self::encryptInternal(
$plaintext,
KeyOrPassword::createFromPassword($password),
$raw_binary
);
}
/**
* Decrypts a ciphertext to a string with a Key.
*
* @param string $ciphertext
* @param Key $key
* @param bool $raw_binary
*
* @throws \TypeError
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
public static function decrypt($ciphertext, $key, $raw_binary = false)
{
if (!\is_string($ciphertext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
);
}
if (!($key instanceof Key)) {
throw new \TypeError(
'Key expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
);
}
if (!\is_bool($raw_binary)) {
throw new \TypeError(
'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
);
}
return self::decryptInternal(
$ciphertext,
KeyOrPassword::createFromKey($key),
$raw_binary
);
}
/**
* Decrypts a ciphertext to a string with a password, using a slow key
* derivation function to make password cracking more expensive.
*
* @param string $ciphertext
* @param string $password
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
* @throws \TypeError
*
* @return string
*/
public static function decryptWithPassword(
$ciphertext,
#[\SensitiveParameter]
$password,
$raw_binary = false
)
{
if (!\is_string($ciphertext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
);
}
if (!\is_string($password)) {
throw new \TypeError(
'String expected for argument 2. ' . \ucfirst(\gettype($password)) . ' given instead.'
);
}
if (!\is_bool($raw_binary)) {
throw new \TypeError(
'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
);
}
return self::decryptInternal(
$ciphertext,
KeyOrPassword::createFromPassword($password),
$raw_binary
);
}
/**
* Decrypts a legacy ciphertext produced by version 1 of this library.
*
* @param string $ciphertext
* @param string $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
* @throws \TypeError
*
* @return string
*/
public static function legacyDecrypt(
$ciphertext,
#[\SensitiveParameter]
$key
)
{
if (!\is_string($ciphertext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
);
}
if (!\is_string($key)) {
throw new \TypeError(
'String expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
);
}
RuntimeTests::runtimeTest();
// Extract the HMAC from the front of the ciphertext.
if (Core::ourStrlen($ciphertext) <= Core::LEGACY_MAC_BYTE_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
/**
* @var string
*/
$hmac = Core::ourSubstr($ciphertext, 0, Core::LEGACY_MAC_BYTE_SIZE);
Core::ensureTrue(\is_string($hmac));
/**
* @var string
*/
$messageCiphertext = Core::ourSubstr($ciphertext, Core::LEGACY_MAC_BYTE_SIZE);
Core::ensureTrue(\is_string($messageCiphertext));
// Regenerate the same authentication sub-key.
$akey = Core::HKDF(
Core::LEGACY_HASH_FUNCTION_NAME,
$key,
Core::LEGACY_KEY_BYTE_SIZE,
Core::LEGACY_AUTHENTICATION_INFO_STRING,
null
);
if (self::verifyHMAC($hmac, $messageCiphertext, $akey)) {
// Regenerate the same encryption sub-key.
$ekey = Core::HKDF(
Core::LEGACY_HASH_FUNCTION_NAME,
$key,
Core::LEGACY_KEY_BYTE_SIZE,
Core::LEGACY_ENCRYPTION_INFO_STRING,
null
);
// Extract the IV from the ciphertext.
if (Core::ourStrlen($messageCiphertext) <= Core::LEGACY_BLOCK_BYTE_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
/**
* @var string
*/
$iv = Core::ourSubstr($messageCiphertext, 0, Core::LEGACY_BLOCK_BYTE_SIZE);
Core::ensureTrue(\is_string($iv));
/**
* @var string
*/
$actualCiphertext = Core::ourSubstr($messageCiphertext, Core::LEGACY_BLOCK_BYTE_SIZE);
Core::ensureTrue(\is_string($actualCiphertext));
// Do the decryption.
$plaintext = self::plainDecrypt($actualCiphertext, $ekey, $iv, Core::LEGACY_CIPHER_METHOD);
return $plaintext;
} else {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Integrity check failed.'
);
}
}
/**
* Encrypts a string with either a key or a password.
*
* @param string $plaintext
* @param KeyOrPassword $secret
* @param bool $raw_binary
*
* @return string
*/
private static function encryptInternal($plaintext, KeyOrPassword $secret, $raw_binary)
{
RuntimeTests::runtimeTest();
$salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
$keys = $secret->deriveKeys($salt);
$ekey = $keys->getEncryptionKey();
$akey = $keys->getAuthenticationKey();
$iv = Core::secureRandom(Core::BLOCK_BYTE_SIZE);
$ciphertext = Core::CURRENT_VERSION . $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv);
$auth = \hash_hmac(Core::HASH_FUNCTION_NAME, $ciphertext, $akey, true);
$ciphertext = $ciphertext . $auth;
if ($raw_binary) {
return $ciphertext;
}
return Encoding::binToHex($ciphertext);
}
/**
* Decrypts a ciphertext to a string with either a key or a password.
*
* @param string $ciphertext
* @param KeyOrPassword $secret
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw_binary)
{
RuntimeTests::runtimeTest();
if (! $raw_binary) {
try {
$ciphertext = Encoding::hexToBin($ciphertext);
} catch (Ex\BadFormatException $ex) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext has invalid hex encoding.'
);
}
}
if (Core::ourStrlen($ciphertext) < Core::MINIMUM_CIPHERTEXT_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
// Get and check the version header.
/** @var string $header */
$header = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE);
if ($header !== Core::CURRENT_VERSION) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Bad version header.'
);
}
// Get the salt.
/** @var string $salt */
$salt = Core::ourSubstr(
$ciphertext,
Core::HEADER_VERSION_SIZE,
Core::SALT_BYTE_SIZE
);
Core::ensureTrue(\is_string($salt));
// Get the IV.
/** @var string $iv */
$iv = Core::ourSubstr(
$ciphertext,
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE,
Core::BLOCK_BYTE_SIZE
);
Core::ensureTrue(\is_string($iv));
// Get the HMAC.
/** @var string $hmac */
$hmac = Core::ourSubstr(
$ciphertext,
Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE,
Core::MAC_BYTE_SIZE
);
Core::ensureTrue(\is_string($hmac));
// Get the actual encrypted ciphertext.
/** @var string $encrypted */
$encrypted = Core::ourSubstr(
$ciphertext,
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE +
Core::BLOCK_BYTE_SIZE,
Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE -
Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE
);
Core::ensureTrue(\is_string($encrypted));
// Derive the separate encryption and authentication keys from the key
// or password, whichever it is.
$keys = $secret->deriveKeys($salt);
if (self::verifyHMAC($hmac, $header . $salt . $iv . $encrypted, $keys->getAuthenticationKey())) {
$plaintext = self::plainDecrypt($encrypted, $keys->getEncryptionKey(), $iv, Core::CIPHER_METHOD);
return $plaintext;
} else {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Integrity check failed.'
);
}
}
/**
* Raw unauthenticated encryption (insecure on its own).
*
* @param string $plaintext
* @param string $key
* @param string $iv
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
protected static function plainEncrypt(
$plaintext,
#[\SensitiveParameter]
$key,
#[\SensitiveParameter]
$iv
)
{
Core::ensureConstantExists('OPENSSL_RAW_DATA');
Core::ensureFunctionExists('openssl_encrypt');
/** @var string $ciphertext */
$ciphertext = \openssl_encrypt(
$plaintext,
Core::CIPHER_METHOD,
$key,
OPENSSL_RAW_DATA,
$iv
);
Core::ensureTrue(\is_string($ciphertext), 'openssl_encrypt() failed');
return $ciphertext;
}
/**
* Raw unauthenticated decryption (insecure on its own).
*
* @param string $ciphertext
* @param string $key
* @param string $iv
* @param string $cipherMethod
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
protected static function plainDecrypt(
$ciphertext,
#[\SensitiveParameter]
$key,
#[\SensitiveParameter]
$iv,
$cipherMethod
)
{
Core::ensureConstantExists('OPENSSL_RAW_DATA');
Core::ensureFunctionExists('openssl_decrypt');
/** @var string $plaintext */
$plaintext = \openssl_decrypt(
$ciphertext,
$cipherMethod,
$key,
OPENSSL_RAW_DATA,
$iv
);
Core::ensureTrue(\is_string($plaintext), 'openssl_decrypt() failed.');
return $plaintext;
}
/**
* Verifies an HMAC without leaking information through side-channels.
*
* @param string $expected_hmac
* @param string $message
* @param string $key
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return bool
*/
protected static function verifyHMAC(
$expected_hmac,
$message,
#[\SensitiveParameter]
$key
)
{
$message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true);
return Core::hashEquals($message_hmac, $expected_hmac);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Defuse\Crypto;
/**
* Class DerivedKeys
* @package Defuse\Crypto
*/
final class DerivedKeys
{
/**
* @var string
*/
private $akey = '';
/**
* @var string
*/
private $ekey = '';
/**
* Returns the authentication key.
* @return string
*/
public function getAuthenticationKey()
{
return $this->akey;
}
/**
* Returns the encryption key.
* @return string
*/
public function getEncryptionKey()
{
return $this->ekey;
}
/**
* Constructor for DerivedKeys.
*
* @param string $akey
* @param string $ekey
*/
public function __construct($akey, $ekey)
{
$this->akey = $akey;
$this->ekey = $ekey;
}
}

View File

@ -0,0 +1,277 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Encoding
{
const CHECKSUM_BYTE_SIZE = 32;
const CHECKSUM_HASH_ALGO = 'sha256';
const SERIALIZE_HEADER_BYTES = 4;
/**
* Converts a byte string to a hexadecimal string without leaking
* information through side channels.
*
* @param string $byte_string
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function binToHex($byte_string)
{
$hex = '';
$len = Core::ourStrlen($byte_string);
for ($i = 0; $i < $len; ++$i) {
$c = \ord($byte_string[$i]) & 0xf;
$b = \ord($byte_string[$i]) >> 4;
$hex .= \pack(
'CC',
87 + $b + ((($b - 10) >> 8) & ~38),
87 + $c + ((($c - 10) >> 8) & ~38)
);
}
return $hex;
}
/**
* Converts a hexadecimal string into a byte string without leaking
* information through side channels.
*
* @param string $hex_string
*
* @throws Ex\BadFormatException
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
* @psalm-suppress TypeDoesNotContainType
*/
public static function hexToBin($hex_string)
{
$hex_pos = 0;
$bin = '';
$hex_len = Core::ourStrlen($hex_string);
$state = 0;
$c_acc = 0;
while ($hex_pos < $hex_len) {
$c = \ord($hex_string[$hex_pos]);
$c_num = $c ^ 48;
$c_num0 = ($c_num - 10) >> 8;
$c_alpha = ($c & ~32) - 55;
$c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
if (($c_num0 | $c_alpha0) === 0) {
throw new Ex\BadFormatException(
'Encoding::hexToBin() input is not a hex string.'
);
}
$c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
if ($state === 0) {
$c_acc = $c_val * 16;
} else {
$bin .= \pack('C', $c_acc | $c_val);
}
$state ^= 1;
++$hex_pos;
}
return $bin;
}
/**
* Remove trialing whitespace without table look-ups or branches.
*
* Calling this function may leak the length of the string as well as the
* number of trailing whitespace characters through side-channels.
*
* @param string $string
* @return string
*/
public static function trimTrailingWhitespace($string = '')
{
$length = Core::ourStrlen($string);
if ($length < 1) {
return '';
}
do {
$prevLength = $length;
$last = $length - 1;
$chr = \ord($string[$last]);
/* Null Byte (0x00), a.k.a. \0 */
// if ($chr === 0x00) $length -= 1;
$sub = (($chr - 1) >> 8 ) & 1;
$length -= $sub;
$last -= $sub;
/* Horizontal Tab (0x09) a.k.a. \t */
$chr = \ord($string[$last]);
// if ($chr === 0x09) $length -= 1;
$sub = (((0x08 - $chr) & ($chr - 0x0a)) >> 8) & 1;
$length -= $sub;
$last -= $sub;
/* New Line (0x0a), a.k.a. \n */
$chr = \ord($string[$last]);
// if ($chr === 0x0a) $length -= 1;
$sub = (((0x09 - $chr) & ($chr - 0x0b)) >> 8) & 1;
$length -= $sub;
$last -= $sub;
/* Carriage Return (0x0D), a.k.a. \r */
$chr = \ord($string[$last]);
// if ($chr === 0x0d) $length -= 1;
$sub = (((0x0c - $chr) & ($chr - 0x0e)) >> 8) & 1;
$length -= $sub;
$last -= $sub;
/* Space */
$chr = \ord($string[$last]);
// if ($chr === 0x20) $length -= 1;
$sub = (((0x1f - $chr) & ($chr - 0x21)) >> 8) & 1;
$length -= $sub;
} while ($prevLength !== $length && $length > 0);
return (string) Core::ourSubstr($string, 0, $length);
}
/*
* SECURITY NOTE ON APPLYING CHECKSUMS TO SECRETS:
*
* The checksum introduces a potential security weakness. For example,
* suppose we apply a checksum to a key, and that an adversary has an
* exploit against the process containing the key, such that they can
* overwrite an arbitrary byte of memory and then cause the checksum to
* be verified and learn the result.
*
* In this scenario, the adversary can extract the key one byte at
* a time by overwriting it with their guess of its value and then
* asking if the checksum matches. If it does, their guess was right.
* This kind of attack may be more easy to implement and more reliable
* than a remote code execution attack.
*
* This attack also applies to authenticated encryption as a whole, in
* the situation where the adversary can overwrite a byte of the key
* and then cause a valid ciphertext to be decrypted, and then
* determine whether the MAC check passed or failed.
*
* By using the full SHA256 hash instead of truncating it, I'm ensuring
* that both ways of going about the attack are equivalently difficult.
* A shorter checksum of say 32 bits might be more useful to the
* adversary as an oracle in case their writes are coarser grained.
*
* Because the scenario assumes a serious vulnerability, we don't try
* to prevent attacks of this style.
*/
/**
* INTERNAL USE ONLY: Applies a version header, applies a checksum, and
* then encodes a byte string into a range of printable ASCII characters.
*
* @param string $header
* @param string $bytes
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function saveBytesToChecksummedAsciiSafeString(
$header,
#[\SensitiveParameter]
$bytes
)
{
// Headers must be a constant length to prevent one type's header from
// being a prefix of another type's header, leading to ambiguity.
Core::ensureTrue(
Core::ourStrlen($header) === self::SERIALIZE_HEADER_BYTES,
'Header must be ' . self::SERIALIZE_HEADER_BYTES . ' bytes.'
);
return Encoding::binToHex(
$header .
$bytes .
\hash(
self::CHECKSUM_HASH_ALGO,
$header . $bytes,
true
)
);
}
/**
* INTERNAL USE ONLY: Decodes, verifies the header and checksum, and returns
* the encoded byte string.
*
* @param string $expected_header
* @param string $string
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\BadFormatException
*
* @return string
*/
public static function loadBytesFromChecksummedAsciiSafeString(
$expected_header,
#[\SensitiveParameter]
$string
)
{
// Headers must be a constant length to prevent one type's header from
// being a prefix of another type's header, leading to ambiguity.
Core::ensureTrue(
Core::ourStrlen($expected_header) === self::SERIALIZE_HEADER_BYTES,
'Header must be 4 bytes.'
);
/* If you get an exception here when attempting to load from a file, first pass your
key to Encoding::trimTrailingWhitespace() to remove newline characters, etc. */
$bytes = Encoding::hexToBin($string);
/* Make sure we have enough bytes to get the version header and checksum. */
if (Core::ourStrlen($bytes) < self::SERIALIZE_HEADER_BYTES + self::CHECKSUM_BYTE_SIZE) {
throw new Ex\BadFormatException(
'Encoded data is shorter than expected.'
);
}
/* Grab the version header. */
$actual_header = (string) Core::ourSubstr($bytes, 0, self::SERIALIZE_HEADER_BYTES);
if ($actual_header !== $expected_header) {
throw new Ex\BadFormatException(
'Invalid header.'
);
}
/* Grab the bytes that are part of the checksum. */
$checked_bytes = (string) Core::ourSubstr(
$bytes,
0,
Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE
);
/* Grab the included checksum. */
$checksum_a = (string) Core::ourSubstr(
$bytes,
Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE,
self::CHECKSUM_BYTE_SIZE
);
/* Re-compute the checksum. */
$checksum_b = \hash(self::CHECKSUM_HASH_ALGO, $checked_bytes, true);
/* Check if the checksum matches. */
if (! Core::hashEquals($checksum_a, $checksum_b)) {
throw new Ex\BadFormatException(
"Data is corrupted, the checksum doesn't match"
);
}
return (string) Core::ourSubstr(
$bytes,
self::SERIALIZE_HEADER_BYTES,
Core::ourStrlen($bytes) - self::SERIALIZE_HEADER_BYTES - self::CHECKSUM_BYTE_SIZE
);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class BadFormatException extends \Defuse\Crypto\Exception\CryptoException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class CryptoException extends \Exception
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class EnvironmentIsBrokenException extends \Defuse\Crypto\Exception\CryptoException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class IOException extends \Defuse\Crypto\Exception\CryptoException
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class WrongKeyOrModifiedCiphertextException extends \Defuse\Crypto\Exception\CryptoException
{
}

View File

@ -0,0 +1,835 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class File
{
/**
* Encrypts the input file, saving the ciphertext to the output file.
*
* @param string $inputFilename
* @param string $outputFilename
* @param Key $key
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
public static function encryptFile($inputFilename, $outputFilename, Key $key)
{
self::encryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromKey($key)
);
}
/**
* Encrypts a file with a password, using a slow key derivation function to
* make password cracking more expensive.
*
* @param string $inputFilename
* @param string $outputFilename
* @param string $password
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
public static function encryptFileWithPassword(
$inputFilename,
$outputFilename,
#[\SensitiveParameter]
$password
)
{
self::encryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Decrypts the input file, saving the plaintext to the output file.
*
* @param string $inputFilename
* @param string $outputFilename
* @param Key $key
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptFile($inputFilename, $outputFilename, Key $key)
{
self::decryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromKey($key)
);
}
/**
* Decrypts a file with a password, using a slow key derivation function to
* make password cracking more expensive.
*
* @param string $inputFilename
* @param string $outputFilename
* @param string $password
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptFileWithPassword(
$inputFilename,
$outputFilename,
#[\SensitiveParameter]
$password
)
{
self::decryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Takes two resource handles and encrypts the contents of the first,
* writing the ciphertext into the second.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param Key $key
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function encryptResource($inputHandle, $outputHandle, Key $key)
{
self::encryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromKey($key)
);
}
/**
* Encrypts the contents of one resource handle into another with a
* password, using a slow key derivation function to make password cracking
* more expensive.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param string $password
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function encryptResourceWithPassword(
$inputHandle,
$outputHandle,
#[\SensitiveParameter]
$password
)
{
self::encryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Takes two resource handles and decrypts the contents of the first,
* writing the plaintext into the second.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param Key $key
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResource($inputHandle, $outputHandle, Key $key)
{
self::decryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromKey($key)
);
}
/**
* Decrypts the contents of one resource into another with a password, using
* a slow key derivation function to make password cracking more expensive.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param string $password
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResourceWithPassword(
$inputHandle,
$outputHandle,
#[\SensitiveParameter]
$password
)
{
self::decryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Encrypts a file with either a key or a password.
*
* @param string $inputFilename
* @param string $outputFilename
* @param KeyOrPassword $secret
* @return void
*
* @throws Ex\CryptoException
* @throws Ex\IOException
*/
private static function encryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
{
if (file_exists($inputFilename) && file_exists($outputFilename) && realpath($inputFilename) === realpath($outputFilename)) {
throw new Ex\IOException('Input and output filenames must be different.');
}
/* Open the input file. */
self::removePHPUnitErrorHandler();
$if = @\fopen($inputFilename, 'rb');
self::restorePHPUnitErrorHandler();
if ($if === false) {
throw new Ex\IOException(
'Cannot open input file for encrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_read_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_read_buffer($if, 0);
}
/* Open the output file. */
self::removePHPUnitErrorHandler();
$of = @\fopen($outputFilename, 'wb');
self::restorePHPUnitErrorHandler();
if ($of === false) {
\fclose($if);
throw new Ex\IOException(
'Cannot open output file for encrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_write_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_write_buffer($of, 0);
}
/* Perform the encryption. */
try {
self::encryptResourceInternal($if, $of, $secret);
} catch (Ex\CryptoException $ex) {
\fclose($if);
\fclose($of);
throw $ex;
}
/* Close the input file. */
if (\fclose($if) === false) {
\fclose($of);
throw new Ex\IOException(
'Cannot close input file after encrypting'
);
}
/* Close the output file. */
if (\fclose($of) === false) {
throw new Ex\IOException(
'Cannot close output file after encrypting'
);
}
}
/**
* Decrypts a file with either a key or a password.
*
* @param string $inputFilename
* @param string $outputFilename
* @param KeyOrPassword $secret
* @return void
*
* @throws Ex\CryptoException
* @throws Ex\IOException
*/
private static function decryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
{
if (file_exists($inputFilename) && file_exists($outputFilename) && realpath($inputFilename) === realpath($outputFilename)) {
throw new Ex\IOException('Input and output filenames must be different.');
}
/* Open the input file. */
self::removePHPUnitErrorHandler();
$if = @\fopen($inputFilename, 'rb');
self::restorePHPUnitErrorHandler();
if ($if === false) {
throw new Ex\IOException(
'Cannot open input file for decrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_read_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_read_buffer($if, 0);
}
/* Open the output file. */
self::removePHPUnitErrorHandler();
$of = @\fopen($outputFilename, 'wb');
self::restorePHPUnitErrorHandler();
if ($of === false) {
\fclose($if);
throw new Ex\IOException(
'Cannot open output file for decrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_write_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_write_buffer($of, 0);
}
/* Perform the decryption. */
try {
self::decryptResourceInternal($if, $of, $secret);
} catch (Ex\CryptoException $ex) {
\fclose($if);
\fclose($of);
throw $ex;
}
/* Close the input file. */
if (\fclose($if) === false) {
\fclose($of);
throw new Ex\IOException(
'Cannot close input file after decrypting'
);
}
/* Close the output file. */
if (\fclose($of) === false) {
throw new Ex\IOException(
'Cannot close output file after decrypting'
);
}
}
/**
* Encrypts a resource with either a key or a password.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param KeyOrPassword $secret
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @psalm-suppress PossiblyInvalidArgument
* Fixes erroneous errors caused by PHP 7.2 switching the return value
* of hash_init from a resource to a HashContext.
*/
private static function encryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
{
if (! \is_resource($inputHandle)) {
throw new Ex\IOException(
'Input handle must be a resource!'
);
}
if (! \is_resource($outputHandle)) {
throw new Ex\IOException(
'Output handle must be a resource!'
);
}
$inputStat = \fstat($inputHandle);
$inputSize = $inputStat['size'];
$file_salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
$keys = $secret->deriveKeys($file_salt);
$ekey = $keys->getEncryptionKey();
$akey = $keys->getAuthenticationKey();
$ivsize = Core::BLOCK_BYTE_SIZE;
$iv = Core::secureRandom($ivsize);
/* Initialize a streaming HMAC state. */
/** @var mixed $hmac */
$hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey);
Core::ensureTrue(
\is_resource($hmac) || \is_object($hmac),
'Cannot initialize a hash context'
);
/* Write the header, salt, and IV. */
self::writeBytes(
$outputHandle,
Core::CURRENT_VERSION . $file_salt . $iv,
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + $ivsize
);
/* Add the header, salt, and IV to the HMAC. */
\hash_update($hmac, Core::CURRENT_VERSION);
\hash_update($hmac, $file_salt);
\hash_update($hmac, $iv);
/* $thisIv will be incremented after each call to the encryption. */
$thisIv = $iv;
/* How many blocks do we encrypt at a time? We increment by this value. */
/**
* @psalm-suppress RedundantCast
*/
$inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE);
/* Loop until we reach the end of the input file. */
$at_file_end = false;
while (! (\feof($inputHandle) || $at_file_end)) {
/* Find out if we can read a full buffer, or only a partial one. */
/** @var int */
$pos = \ftell($inputHandle);
if (!\is_int($pos)) {
throw new Ex\IOException(
'Could not get current position in input file during encryption'
);
}
if ($pos + Core::BUFFER_BYTE_SIZE >= $inputSize) {
/* We're at the end of the file, so we need to break out of the loop. */
$at_file_end = true;
$read = self::readBytes(
$inputHandle,
$inputSize - $pos
);
} else {
$read = self::readBytes(
$inputHandle,
Core::BUFFER_BYTE_SIZE
);
}
/* Encrypt this buffer. */
/** @var string */
$encrypted = \openssl_encrypt(
$read,
Core::CIPHER_METHOD,
$ekey,
OPENSSL_RAW_DATA,
$thisIv
);
Core::ensureTrue(\is_string($encrypted), 'OpenSSL encryption error');
/* Write this buffer's ciphertext. */
self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted));
/* Add this buffer's ciphertext to the HMAC. */
\hash_update($hmac, $encrypted);
/* Increment the counter by the number of blocks in a buffer. */
$thisIv = Core::incrementCounter($thisIv, $inc);
/* WARNING: Usually, unless the file is a multiple of the buffer
* size, $thisIv will contain an incorrect value here on the last
* iteration of this loop. */
}
/* Get the HMAC and append it to the ciphertext. */
$final_mac = \hash_final($hmac, true);
self::writeBytes($outputHandle, $final_mac, Core::MAC_BYTE_SIZE);
}
/**
* Decrypts a file-backed resource with either a key or a password.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param KeyOrPassword $secret
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
* @psalm-suppress PossiblyInvalidArgument
* Fixes erroneous errors caused by PHP 7.2 switching the return value
* of hash_init from a resource to a HashContext.
*/
public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
{
if (! \is_resource($inputHandle)) {
throw new Ex\IOException(
'Input handle must be a resource!'
);
}
if (! \is_resource($outputHandle)) {
throw new Ex\IOException(
'Output handle must be a resource!'
);
}
/* Make sure the file is big enough for all the reads we need to do. */
$stat = \fstat($inputHandle);
if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Input file is too small to have been created by this library.'
);
}
/* Check the version header. */
$header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE);
if ($header !== Core::CURRENT_VERSION) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Bad version header.'
);
}
/* Get the salt. */
$file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE);
/* Get the IV. */
$ivsize = Core::BLOCK_BYTE_SIZE;
$iv = self::readBytes($inputHandle, $ivsize);
/* Derive the authentication and encryption keys. */
$keys = $secret->deriveKeys($file_salt);
$ekey = $keys->getEncryptionKey();
$akey = $keys->getAuthenticationKey();
/* We'll store the MAC of each buffer-sized chunk as we verify the
* actual MAC, so that we can check them again when decrypting. */
$macs = [];
/* $thisIv will be incremented after each call to the decryption. */
$thisIv = $iv;
/* How many blocks do we encrypt at a time? We increment by this value. */
/**
* @psalm-suppress RedundantCast
*/
$inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE);
/* Get the HMAC. */
if (\fseek($inputHandle, (-1 * Core::MAC_BYTE_SIZE), SEEK_END) === -1) {
throw new Ex\IOException(
'Cannot seek to beginning of MAC within input file'
);
}
/* Get the position of the last byte in the actual ciphertext. */
/** @var int $cipher_end */
$cipher_end = \ftell($inputHandle);
if (!\is_int($cipher_end)) {
throw new Ex\IOException(
'Cannot read input file'
);
}
/* We have the position of the first byte of the HMAC. Go back by one. */
--$cipher_end;
/* Read the HMAC. */
/** @var string $stored_mac */
$stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE);
/* Initialize a streaming HMAC state. */
/** @var mixed $hmac */
$hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey);
Core::ensureTrue(\is_resource($hmac) || \is_object($hmac), 'Cannot initialize a hash context');
/* Reset file pointer to the beginning of the file after the header */
if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === -1) {
throw new Ex\IOException(
'Cannot read seek within input file'
);
}
/* Seek to the start of the actual ciphertext. */
if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === -1) {
throw new Ex\IOException(
'Cannot seek input file to beginning of ciphertext'
);
}
/* PASS #1: Calculating the HMAC. */
\hash_update($hmac, $header);
\hash_update($hmac, $file_salt);
\hash_update($hmac, $iv);
/** @var mixed $hmac2 */
$hmac2 = \hash_copy($hmac);
$break = false;
while (! $break) {
/** @var int $pos */
$pos = \ftell($inputHandle);
if (!\is_int($pos)) {
throw new Ex\IOException(
'Could not get current position in input file during decryption'
);
}
/* Read the next buffer-sized chunk (or less). */
if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
$break = true;
$read = self::readBytes(
$inputHandle,
$cipher_end - $pos + 1
);
} else {
$read = self::readBytes(
$inputHandle,
Core::BUFFER_BYTE_SIZE
);
}
/* Update the HMAC. */
\hash_update($hmac, $read);
/* Remember this buffer-sized chunk's HMAC. */
/** @var mixed $chunk_mac */
$chunk_mac = \hash_copy($hmac);
Core::ensureTrue(\is_resource($chunk_mac) || \is_object($chunk_mac), 'Cannot duplicate a hash context');
$macs []= \hash_final($chunk_mac);
}
/* Get the final HMAC, which should match the stored one. */
/** @var string $final_mac */
$final_mac = \hash_final($hmac, true);
/* Verify the HMAC. */
if (! Core::hashEquals($final_mac, $stored_mac)) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Integrity check failed.'
);
}
/* PASS #2: Decrypt and write output. */
/* Rewind to the start of the actual ciphertext. */
if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === -1) {
throw new Ex\IOException(
'Could not move the input file pointer during decryption'
);
}
$at_file_end = false;
while (! $at_file_end) {
/** @var int $pos */
$pos = \ftell($inputHandle);
if (!\is_int($pos)) {
throw new Ex\IOException(
'Could not get current position in input file during decryption'
);
}
/* Read the next buffer-sized chunk (or less). */
if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
$at_file_end = true;
$read = self::readBytes(
$inputHandle,
$cipher_end - $pos + 1
);
} else {
$read = self::readBytes(
$inputHandle,
Core::BUFFER_BYTE_SIZE
);
}
/* Recalculate the MAC (so far) and compare it with the one we
* remembered from pass #1 to ensure attackers didn't change the
* ciphertext after MAC verification. */
\hash_update($hmac2, $read);
/** @var mixed $calc_mac */
$calc_mac = \hash_copy($hmac2);
Core::ensureTrue(\is_resource($calc_mac) || \is_object($calc_mac), 'Cannot duplicate a hash context');
$calc = \hash_final($calc_mac);
if (empty($macs)) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'File was modified after MAC verification'
);
} elseif (! Core::hashEquals(\array_shift($macs), $calc)) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'File was modified after MAC verification'
);
}
/* Decrypt this buffer-sized chunk. */
/** @var string $decrypted */
$decrypted = \openssl_decrypt(
$read,
Core::CIPHER_METHOD,
$ekey,
OPENSSL_RAW_DATA,
$thisIv
);
Core::ensureTrue(\is_string($decrypted), 'OpenSSL decryption error');
/* Write the plaintext to the output file. */
self::writeBytes(
$outputHandle,
$decrypted,
Core::ourStrlen($decrypted)
);
/* Increment the IV by the amount of blocks in a buffer. */
/** @var string $thisIv */
$thisIv = Core::incrementCounter($thisIv, $inc);
/* WARNING: Usually, unless the file is a multiple of the buffer
* size, $thisIv will contain an incorrect value here on the last
* iteration of this loop. */
}
}
/**
* Read from a stream; prevent partial reads.
*
* @param resource $stream
* @param int $num_bytes
* @return string
*
* @throws Ex\IOException
* @throws Ex\EnvironmentIsBrokenException
*/
public static function readBytes($stream, $num_bytes)
{
Core::ensureTrue($num_bytes >= 0, 'Tried to read less than 0 bytes');
if ($num_bytes === 0) {
return '';
}
$buf = '';
$remaining = $num_bytes;
while ($remaining > 0 && ! \feof($stream)) {
/** @var string $read */
$read = \fread($stream, $remaining);
if (!\is_string($read)) {
throw new Ex\IOException(
'Could not read from the file'
);
}
$buf .= $read;
$remaining -= Core::ourStrlen($read);
}
if (Core::ourStrlen($buf) !== $num_bytes) {
throw new Ex\IOException(
'Tried to read past the end of the file'
);
}
return $buf;
}
/**
* Write to a stream; prevents partial writes.
*
* @param resource $stream
* @param string $buf
* @param int $num_bytes
* @return int
*
* @throws Ex\IOException
*/
public static function writeBytes($stream, $buf, $num_bytes = null)
{
$bufSize = Core::ourStrlen($buf);
if ($num_bytes === null) {
$num_bytes = $bufSize;
}
if ($num_bytes > $bufSize) {
throw new Ex\IOException(
'Trying to write more bytes than the buffer contains.'
);
}
if ($num_bytes < 0) {
throw new Ex\IOException(
'Tried to write less than 0 bytes'
);
}
$remaining = $num_bytes;
while ($remaining > 0) {
/** @var int $written */
$written = \fwrite($stream, $buf, $remaining);
if (!\is_int($written)) {
throw new Ex\IOException(
'Could not write to the file'
);
}
$buf = (string) Core::ourSubstr($buf, $written, null);
$remaining -= $written;
}
return $num_bytes;
}
/**
* Returns the last PHP error's or warning's message string.
*
* @return string
*/
private static function getLastErrorMessage()
{
$error = error_get_last();
if ($error === null) {
return '[no PHP error, or you have a custom error handler set]';
} else {
return $error['message'];
}
}
/**
* PHPUnit sets an error handler, which prevents getLastErrorMessage() from working,
* because error_get_last does not work when custom handlers are set.
*
* This is a workaround, which should be a no-op in production deployments, to make
* getLastErrorMessage() return the error messages that the PHPUnit tests expect.
*
* If, in a production deployment, a custom error handler is set, the exception
* handling will still work as usual, but the error messages will be confusing.
*
* @return void
*/
private static function removePHPUnitErrorHandler() {
if (defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__')) {
set_error_handler(null);
}
}
/**
* Undoes what removePHPUnitErrorHandler did.
*
* @return void
*/
private static function restorePHPUnitErrorHandler() {
if (defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__')) {
restore_error_handler();
}
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Key
{
const KEY_CURRENT_VERSION = "\xDE\xF0\x00\x00";
const KEY_BYTE_SIZE = 32;
/**
* @var string
*/
private $key_bytes;
/**
* Creates new random key.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return Key
*/
public static function createNewRandomKey()
{
return new Key(Core::secureRandom(self::KEY_BYTE_SIZE));
}
/**
* Loads a Key from its encoded form.
*
* By default, this function will call Encoding::trimTrailingWhitespace()
* to remove trailing CR, LF, NUL, TAB, and SPACE characters, which are
* commonly appended to files when working with text editors.
*
* @param string $saved_key_string
* @param bool $do_not_trim (default: false)
*
* @throws Ex\BadFormatException
* @throws Ex\EnvironmentIsBrokenException
*
* @return Key
*/
public static function loadFromAsciiSafeString(
#[\SensitiveParameter]
$saved_key_string,
$do_not_trim = false
)
{
if (!$do_not_trim) {
$saved_key_string = Encoding::trimTrailingWhitespace($saved_key_string);
}
$key_bytes = Encoding::loadBytesFromChecksummedAsciiSafeString(self::KEY_CURRENT_VERSION, $saved_key_string);
return new Key($key_bytes);
}
/**
* Encodes the Key into a string of printable ASCII characters.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public function saveToAsciiSafeString()
{
return Encoding::saveBytesToChecksummedAsciiSafeString(
self::KEY_CURRENT_VERSION,
$this->key_bytes
);
}
/**
* Gets the raw bytes of the key.
*
* @return string
*/
public function getRawBytes()
{
return $this->key_bytes;
}
/**
* Constructs a new Key object from a string of raw bytes.
*
* @param string $bytes
*
* @throws Ex\EnvironmentIsBrokenException
*/
private function __construct(
#[\SensitiveParameter]
$bytes
)
{
Core::ensureTrue(
Core::ourStrlen($bytes) === self::KEY_BYTE_SIZE,
'Bad key length.'
);
$this->key_bytes = $bytes;
}
}

View File

@ -0,0 +1,156 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class KeyOrPassword
{
const PBKDF2_ITERATIONS = 100000;
const SECRET_TYPE_KEY = 1;
const SECRET_TYPE_PASSWORD = 2;
/**
* @var int
*/
private $secret_type = 0;
/**
* @var Key|string
*/
private $secret;
/**
* Initializes an instance of KeyOrPassword from a key.
*
* @param Key $key
*
* @return KeyOrPassword
*/
public static function createFromKey(Key $key)
{
return new KeyOrPassword(self::SECRET_TYPE_KEY, $key);
}
/**
* Initializes an instance of KeyOrPassword from a password.
*
* @param string $password
*
* @return KeyOrPassword
*/
public static function createFromPassword(
#[\SensitiveParameter]
$password
)
{
return new KeyOrPassword(self::SECRET_TYPE_PASSWORD, $password);
}
/**
* Derives authentication and encryption keys from the secret, using a slow
* key derivation function if the secret is a password.
*
* @param string $salt
*
* @throws Ex\CryptoException
* @throws Ex\EnvironmentIsBrokenException
*
* @return DerivedKeys
*/
public function deriveKeys($salt)
{
Core::ensureTrue(
Core::ourStrlen($salt) === Core::SALT_BYTE_SIZE,
'Bad salt.'
);
if ($this->secret_type === self::SECRET_TYPE_KEY) {
Core::ensureTrue($this->secret instanceof Key);
/**
* @psalm-suppress PossiblyInvalidMethodCall
*/
$akey = Core::HKDF(
Core::HASH_FUNCTION_NAME,
$this->secret->getRawBytes(),
Core::KEY_BYTE_SIZE,
Core::AUTHENTICATION_INFO_STRING,
$salt
);
/**
* @psalm-suppress PossiblyInvalidMethodCall
*/
$ekey = Core::HKDF(
Core::HASH_FUNCTION_NAME,
$this->secret->getRawBytes(),
Core::KEY_BYTE_SIZE,
Core::ENCRYPTION_INFO_STRING,
$salt
);
return new DerivedKeys($akey, $ekey);
} elseif ($this->secret_type === self::SECRET_TYPE_PASSWORD) {
Core::ensureTrue(\is_string($this->secret));
/* Our PBKDF2 polyfill is vulnerable to a DoS attack documented in
* GitHub issue #230. The fix is to pre-hash the password to ensure
* it is short. We do the prehashing here instead of in pbkdf2() so
* that pbkdf2() still computes the function as defined by the
* standard. */
/**
* @psalm-suppress PossiblyInvalidArgument
*/
$prehash = \hash(Core::HASH_FUNCTION_NAME, $this->secret, true);
$prekey = Core::pbkdf2(
Core::HASH_FUNCTION_NAME,
$prehash,
$salt,
self::PBKDF2_ITERATIONS,
Core::KEY_BYTE_SIZE,
true
);
$akey = Core::HKDF(
Core::HASH_FUNCTION_NAME,
$prekey,
Core::KEY_BYTE_SIZE,
Core::AUTHENTICATION_INFO_STRING,
$salt
);
/* Note the cryptographic re-use of $salt here. */
$ekey = Core::HKDF(
Core::HASH_FUNCTION_NAME,
$prekey,
Core::KEY_BYTE_SIZE,
Core::ENCRYPTION_INFO_STRING,
$salt
);
return new DerivedKeys($akey, $ekey);
} else {
throw new Ex\EnvironmentIsBrokenException('Bad secret type.');
}
}
/**
* Constructor for KeyOrPassword.
*
* @param int $secret_type
* @param mixed $secret (either a Key or a password string)
*/
private function __construct(
$secret_type,
#[\SensitiveParameter]
$secret
)
{
// The constructor is private, so these should never throw.
if ($secret_type === self::SECRET_TYPE_KEY) {
Core::ensureTrue($secret instanceof Key);
} elseif ($secret_type === self::SECRET_TYPE_PASSWORD) {
Core::ensureTrue(\is_string($secret));
} else {
throw new Ex\EnvironmentIsBrokenException('Bad secret type.');
}
$this->secret_type = $secret_type;
$this->secret = $secret;
}
}

View File

@ -0,0 +1,159 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class KeyProtectedByPassword
{
const PASSWORD_KEY_CURRENT_VERSION = "\xDE\xF1\x00\x00";
/**
* @var string
*/
private $encrypted_key = '';
/**
* Creates a random key protected by the provided password.
*
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return KeyProtectedByPassword
*/
public static function createRandomPasswordProtectedKey(
#[\SensitiveParameter]
$password
)
{
$inner_key = Key::createNewRandomKey();
/* The password is hashed as a form of poor-man's domain separation
* between this use of encryptWithPassword() and other uses of
* encryptWithPassword() that the user may also be using as part of the
* same protocol. */
$encrypted_key = Crypto::encryptWithPassword(
$inner_key->saveToAsciiSafeString(),
\hash(Core::HASH_FUNCTION_NAME, $password, true),
true
);
return new KeyProtectedByPassword($encrypted_key);
}
/**
* Loads a KeyProtectedByPassword from its encoded form.
*
* @param string $saved_key_string
*
* @throws Ex\BadFormatException
*
* @return KeyProtectedByPassword
*/
public static function loadFromAsciiSafeString(
#[\SensitiveParameter]
$saved_key_string
)
{
$encrypted_key = Encoding::loadBytesFromChecksummedAsciiSafeString(
self::PASSWORD_KEY_CURRENT_VERSION,
$saved_key_string
);
return new KeyProtectedByPassword($encrypted_key);
}
/**
* Encodes the KeyProtectedByPassword into a string of printable ASCII
* characters.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public function saveToAsciiSafeString()
{
return Encoding::saveBytesToChecksummedAsciiSafeString(
self::PASSWORD_KEY_CURRENT_VERSION,
$this->encrypted_key
);
}
/**
* Decrypts the protected key, returning an unprotected Key object that can
* be used for encryption and decryption.
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @param string $password
* @return Key
*/
public function unlockKey(
#[\SensitiveParameter]
$password
)
{
try {
$inner_key_encoded = Crypto::decryptWithPassword(
$this->encrypted_key,
\hash(Core::HASH_FUNCTION_NAME, $password, true),
true
);
return Key::loadFromAsciiSafeString($inner_key_encoded);
} catch (Ex\BadFormatException $ex) {
/* This should never happen unless an attacker replaced the
* encrypted key ciphertext with some other ciphertext that was
* encrypted with the same password. We transform the exception type
* here in order to make the API simpler, avoiding the need to
* document that this method might throw an Ex\BadFormatException. */
throw new Ex\WrongKeyOrModifiedCiphertextException(
"The decrypted key was found to be in an invalid format. " .
"This very likely indicates it was modified by an attacker."
);
}
}
/**
* Changes the password.
*
* @param string $current_password
* @param string $new_password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return KeyProtectedByPassword
*/
public function changePassword(
#[\SensitiveParameter]
$current_password,
#[\SensitiveParameter]
$new_password
)
{
$inner_key = $this->unlockKey($current_password);
/* The password is hashed as a form of poor-man's domain separation
* between this use of encryptWithPassword() and other uses of
* encryptWithPassword() that the user may also be using as part of the
* same protocol. */
$encrypted_key = Crypto::encryptWithPassword(
$inner_key->saveToAsciiSafeString(),
\hash(Core::HASH_FUNCTION_NAME, $new_password, true),
true
);
$this->encrypted_key = $encrypted_key;
return $this;
}
/**
* Constructor for KeyProtectedByPassword.
*
* @param string $encrypted_key
*/
private function __construct($encrypted_key)
{
$this->encrypted_key = $encrypted_key;
}
}

View File

@ -0,0 +1,228 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
/*
* We're using static class inheritance to get access to protected methods
* inside Crypto. To make it easy to know where the method we're calling can be
* found, within this file, prefix calls with `Crypto::` or `RuntimeTests::`,
* and don't use `self::`.
*/
class RuntimeTests extends Crypto
{
/**
* Runs the runtime tests.
*
* @throws Ex\EnvironmentIsBrokenException
* @return void
*/
public static function runtimeTest()
{
// 0: Tests haven't been run yet.
// 1: Tests have passed.
// 2: Tests are running right now.
// 3: Tests have failed.
static $test_state = 0;
if ($test_state === 1 || $test_state === 2) {
return;
}
if ($test_state === 3) {
/* If an intermittent problem caused a test to fail previously, we
* want that to be indicated to the user with every call to this
* library. This way, if the user first does something they really
* don't care about, and just ignores all exceptions, they won't get
* screwed when they then start to use the library for something
* they do care about. */
throw new Ex\EnvironmentIsBrokenException('Tests failed previously.');
}
try {
$test_state = 2;
Core::ensureFunctionExists('openssl_get_cipher_methods');
if (\in_array(Core::CIPHER_METHOD, \openssl_get_cipher_methods()) === false) {
throw new Ex\EnvironmentIsBrokenException(
'Cipher method not supported. This is normally caused by an outdated ' .
'version of OpenSSL (and/or OpenSSL compiled for FIPS compliance). ' .
'Please upgrade to a newer version of OpenSSL that supports ' .
Core::CIPHER_METHOD . ' to use this library.'
);
}
RuntimeTests::AESTestVector();
RuntimeTests::HMACTestVector();
RuntimeTests::HKDFTestVector();
RuntimeTests::testEncryptDecrypt();
Core::ensureTrue(Core::ourStrlen(Key::createNewRandomKey()->getRawBytes()) === Core::KEY_BYTE_SIZE);
Core::ensureTrue(Core::ENCRYPTION_INFO_STRING !== Core::AUTHENTICATION_INFO_STRING);
} catch (Ex\EnvironmentIsBrokenException $ex) {
// Do this, otherwise it will stay in the "tests are running" state.
$test_state = 3;
throw $ex;
}
// Change this to '0' make the tests always re-run (for benchmarking).
$test_state = 1;
}
/**
* High-level tests of Crypto operations.
*
* @throws Ex\EnvironmentIsBrokenException
* @return void
*/
private static function testEncryptDecrypt()
{
$key = Key::createNewRandomKey();
$data = "EnCrYpT EvErYThInG\x00\x00";
// Make sure encrypting then decrypting doesn't change the message.
$ciphertext = Crypto::encrypt($data, $key, true);
try {
$decrypted = Crypto::decrypt($ciphertext, $key, true);
} catch (Ex\WrongKeyOrModifiedCiphertextException $ex) {
// It's important to catch this and change it into a
// Ex\EnvironmentIsBrokenException, otherwise a test failure could trick
// the user into thinking it's just an invalid ciphertext!
throw new Ex\EnvironmentIsBrokenException();
}
Core::ensureTrue($decrypted === $data);
// Modifying the ciphertext: Appending a string.
try {
Crypto::decrypt($ciphertext . 'a', $key, true);
throw new Ex\EnvironmentIsBrokenException();
} catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */
}
// Modifying the ciphertext: Changing an HMAC byte.
$indices_to_change = [
0, // The header.
Core::HEADER_VERSION_SIZE + 1, // the salt
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + 1, // the IV
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE + 1, // the ciphertext
];
foreach ($indices_to_change as $index) {
try {
$ciphertext[$index] = \chr((\ord($ciphertext[$index]) + 1) % 256);
Crypto::decrypt($ciphertext, $key, true);
throw new Ex\EnvironmentIsBrokenException();
} catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */
}
}
// Decrypting with the wrong key.
$key = Key::createNewRandomKey();
$data = 'abcdef';
$ciphertext = Crypto::encrypt($data, $key, true);
$wrong_key = Key::createNewRandomKey();
try {
Crypto::decrypt($ciphertext, $wrong_key, true);
throw new Ex\EnvironmentIsBrokenException();
} catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */
}
// Ciphertext too small.
$key = Key::createNewRandomKey();
$ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1);
try {
Crypto::decrypt($ciphertext, $key, true);
throw new Ex\EnvironmentIsBrokenException();
} catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */
}
}
/**
* Test HKDF against test vectors.
*
* @throws Ex\EnvironmentIsBrokenException
* @return void
*/
private static function HKDFTestVector()
{
// HKDF test vectors from RFC 5869
// Test Case 1
$ikm = \str_repeat("\x0b", 22);
$salt = Encoding::hexToBin('000102030405060708090a0b0c');
$info = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9');
$length = 42;
$okm = Encoding::hexToBin(
'3cb25f25faacd57a90434f64d0362f2a' .
'2d2d0a90cf1a5a4c5db02d56ecc4c5bf' .
'34007208d5b887185865'
);
$computed_okm = Core::HKDF('sha256', $ikm, $length, $info, $salt);
Core::ensureTrue($computed_okm === $okm);
// Test Case 7
$ikm = \str_repeat("\x0c", 22);
$length = 42;
$okm = Encoding::hexToBin(
'2c91117204d745f3500d636a62f64f0a' .
'b3bae548aa53d423b0d1f27ebba6f5e5' .
'673a081d70cce7acfc48'
);
$computed_okm = Core::HKDF('sha1', $ikm, $length, '', null);
Core::ensureTrue($computed_okm === $okm);
}
/**
* Test HMAC against test vectors.
*
* @throws Ex\EnvironmentIsBrokenException
* @return void
*/
private static function HMACTestVector()
{
// HMAC test vector From RFC 4231 (Test Case 1)
$key = \str_repeat("\x0b", 20);
$data = 'Hi There';
$correct = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7';
Core::ensureTrue(
\hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) === $correct
);
}
/**
* Test AES against test vectors.
*
* @throws Ex\EnvironmentIsBrokenException
* @return void
*/
private static function AESTestVector()
{
// AES CTR mode test vector from NIST SP 800-38A
$key = Encoding::hexToBin(
'603deb1015ca71be2b73aef0857d7781' .
'1f352c073b6108d72d9810a30914dff4'
);
$iv = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff');
$plaintext = Encoding::hexToBin(
'6bc1bee22e409f96e93d7e117393172a' .
'ae2d8a571e03ac9c9eb76fac45af8e51' .
'30c81c46a35ce411e5fbc1191a0a52ef' .
'f69f2445df4f9b17ad2b417be66c3710'
);
$ciphertext = Encoding::hexToBin(
'601ec313775789a5b7a7f504bbf3d228' .
'f443e3ca4d62b59aca84e990cacaf5c5' .
'2b0930daa23de94ce87017ba2d84988d' .
'dfc9c58db67aada613c2dd08457941a6'
);
$computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv);
Core::ensureTrue($computed_ciphertext === $ciphertext);
$computed_plaintext = Crypto::plainDecrypt($ciphertext, $key, $iv, Core::CIPHER_METHOD);
Core::ensureTrue($computed_plaintext === $plaintext);
}
}

View File

@ -0,0 +1 @@
vendor/

View File

@ -0,0 +1,147 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.1.5 - 2020-11-24
### Added
- [#19](https://github.com/php-fig/http-message-util/pull/19) adds support for PHP 8.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.4 - 2020-02-05
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- [#15](https://github.com/php-fig/http-message-util/pull/15) removes the dependency on psr/http-message, as it is not technically necessary for usage of this package.
### Fixed
- Nothing.
## 1.1.3 - 2018-11-19
### Added
- [#10](https://github.com/php-fig/http-message-util/pull/10) adds the constants `StatusCodeInterface::STATUS_EARLY_HINTS` (103) and
`StatusCodeInterface::STATUS_TOO_EARLY` (425).
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.2 - 2017-02-09
### Added
- [#4](https://github.com/php-fig/http-message-util/pull/4) adds the constant
`StatusCodeInterface::STATUS_MISDIRECTED_REQUEST` (421).
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.1 - 2017-02-06
### Added
- [#3](https://github.com/php-fig/http-message-util/pull/3) adds the constant
`StatusCodeInterface::STATUS_IM_A_TEAPOT` (418).
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.1.0 - 2016-09-19
### Added
- [#1](https://github.com/php-fig/http-message-util/pull/1) adds
`Fig\Http\Message\StatusCodeInterface`, with constants named after common
status reason phrases, with values indicating the status codes themselves.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.0.0 - 2017-08-05
### Added
- Adds `Fig\Http\Message\RequestMethodInterface`, with constants covering the
most common HTTP request methods as specified by the IETF.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.

View File

@ -0,0 +1,19 @@
Copyright (c) 2016 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,17 @@
# PSR Http Message Util
This repository holds utility classes and constants to facilitate common
operations of [PSR-7](https://www.php-fig.org/psr/psr-7/); the primary purpose is
to provide constants for referring to request methods, response status codes and
messages, and potentially common headers.
Implementation of PSR-7 interfaces is **not** within the scope of this package.
## Installation
Install by adding the package as a [Composer](https://getcomposer.org)
requirement:
```bash
$ composer require fig/http-message-util
```

View File

@ -0,0 +1,28 @@
{
"name": "fig/http-message-util",
"description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
"keywords": ["psr", "psr-7", "http", "http-message", "request", "response"],
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"require": {
"php": "^5.3 || ^7.0 || ^8.0"
},
"suggest": {
"psr/http-message": "The package containing the PSR-7 interfaces"
},
"autoload": {
"psr-4": {
"Fig\\Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Fig\Http\Message;
/**
* Defines constants for common HTTP request methods.
*
* Usage:
*
* <code>
* class RequestFactory implements RequestMethodInterface
* {
* public static function factory(
* $uri = '/',
* $method = self::METHOD_GET,
* $data = []
* ) {
* }
* }
* </code>
*/
interface RequestMethodInterface
{
const METHOD_HEAD = 'HEAD';
const METHOD_GET = 'GET';
const METHOD_POST = 'POST';
const METHOD_PUT = 'PUT';
const METHOD_PATCH = 'PATCH';
const METHOD_DELETE = 'DELETE';
const METHOD_PURGE = 'PURGE';
const METHOD_OPTIONS = 'OPTIONS';
const METHOD_TRACE = 'TRACE';
const METHOD_CONNECT = 'CONNECT';
}

View File

@ -0,0 +1,107 @@
<?php
namespace Fig\Http\Message;
/**
* Defines constants for common HTTP status code.
*
* @see https://tools.ietf.org/html/rfc2295#section-8.1
* @see https://tools.ietf.org/html/rfc2324#section-2.3
* @see https://tools.ietf.org/html/rfc2518#section-9.7
* @see https://tools.ietf.org/html/rfc2774#section-7
* @see https://tools.ietf.org/html/rfc3229#section-10.4
* @see https://tools.ietf.org/html/rfc4918#section-11
* @see https://tools.ietf.org/html/rfc5842#section-7.1
* @see https://tools.ietf.org/html/rfc5842#section-7.2
* @see https://tools.ietf.org/html/rfc6585#section-3
* @see https://tools.ietf.org/html/rfc6585#section-4
* @see https://tools.ietf.org/html/rfc6585#section-5
* @see https://tools.ietf.org/html/rfc6585#section-6
* @see https://tools.ietf.org/html/rfc7231#section-6
* @see https://tools.ietf.org/html/rfc7238#section-3
* @see https://tools.ietf.org/html/rfc7725#section-3
* @see https://tools.ietf.org/html/rfc7540#section-9.1.2
* @see https://tools.ietf.org/html/rfc8297#section-2
* @see https://tools.ietf.org/html/rfc8470#section-7
* Usage:
*
* <code>
* class ResponseFactory implements StatusCodeInterface
* {
* public function createResponse($code = self::STATUS_OK)
* {
* }
* }
* </code>
*/
interface StatusCodeInterface
{
// Informational 1xx
const STATUS_CONTINUE = 100;
const STATUS_SWITCHING_PROTOCOLS = 101;
const STATUS_PROCESSING = 102;
const STATUS_EARLY_HINTS = 103;
// Successful 2xx
const STATUS_OK = 200;
const STATUS_CREATED = 201;
const STATUS_ACCEPTED = 202;
const STATUS_NON_AUTHORITATIVE_INFORMATION = 203;
const STATUS_NO_CONTENT = 204;
const STATUS_RESET_CONTENT = 205;
const STATUS_PARTIAL_CONTENT = 206;
const STATUS_MULTI_STATUS = 207;
const STATUS_ALREADY_REPORTED = 208;
const STATUS_IM_USED = 226;
// Redirection 3xx
const STATUS_MULTIPLE_CHOICES = 300;
const STATUS_MOVED_PERMANENTLY = 301;
const STATUS_FOUND = 302;
const STATUS_SEE_OTHER = 303;
const STATUS_NOT_MODIFIED = 304;
const STATUS_USE_PROXY = 305;
const STATUS_RESERVED = 306;
const STATUS_TEMPORARY_REDIRECT = 307;
const STATUS_PERMANENT_REDIRECT = 308;
// Client Errors 4xx
const STATUS_BAD_REQUEST = 400;
const STATUS_UNAUTHORIZED = 401;
const STATUS_PAYMENT_REQUIRED = 402;
const STATUS_FORBIDDEN = 403;
const STATUS_NOT_FOUND = 404;
const STATUS_METHOD_NOT_ALLOWED = 405;
const STATUS_NOT_ACCEPTABLE = 406;
const STATUS_PROXY_AUTHENTICATION_REQUIRED = 407;
const STATUS_REQUEST_TIMEOUT = 408;
const STATUS_CONFLICT = 409;
const STATUS_GONE = 410;
const STATUS_LENGTH_REQUIRED = 411;
const STATUS_PRECONDITION_FAILED = 412;
const STATUS_PAYLOAD_TOO_LARGE = 413;
const STATUS_URI_TOO_LONG = 414;
const STATUS_UNSUPPORTED_MEDIA_TYPE = 415;
const STATUS_RANGE_NOT_SATISFIABLE = 416;
const STATUS_EXPECTATION_FAILED = 417;
const STATUS_IM_A_TEAPOT = 418;
const STATUS_MISDIRECTED_REQUEST = 421;
const STATUS_UNPROCESSABLE_ENTITY = 422;
const STATUS_LOCKED = 423;
const STATUS_FAILED_DEPENDENCY = 424;
const STATUS_TOO_EARLY = 425;
const STATUS_UPGRADE_REQUIRED = 426;
const STATUS_PRECONDITION_REQUIRED = 428;
const STATUS_TOO_MANY_REQUESTS = 429;
const STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
const STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
// Server Errors 5xx
const STATUS_INTERNAL_SERVER_ERROR = 500;
const STATUS_NOT_IMPLEMENTED = 501;
const STATUS_BAD_GATEWAY = 502;
const STATUS_SERVICE_UNAVAILABLE = 503;
const STATUS_GATEWAY_TIMEOUT = 504;
const STATUS_VERSION_NOT_SUPPORTED = 505;
const STATUS_VARIANT_ALSO_NEGOTIATES = 506;
const STATUS_INSUFFICIENT_STORAGE = 507;
const STATUS_LOOP_DETECTED = 508;
const STATUS_NOT_EXTENDED = 510;
const STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511;
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Luís Cobucci
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,48 @@
{
"name": "lcobucci/clock",
"description": "Yet another clock abstraction",
"license": "MIT",
"type": "library",
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com"
}
],
"require": {
"php": "~8.1.0 || ~8.2.0",
"stella-maris/clock": "^0.1.7"
},
"require-dev": {
"infection/infection": "^0.26",
"lcobucci/coding-standard": "^9.0",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.9.4",
"phpstan/phpstan-deprecation-rules": "^1.1.1",
"phpstan/phpstan-phpunit": "^1.3.2",
"phpstan/phpstan-strict-rules": "^1.4.4",
"phpunit/phpunit": "^9.5.27"
},
"autoload": {
"psr-4": {
"Lcobucci\\Clock\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Lcobucci\\Clock\\": "test"
}
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"infection/extension-installer": true,
"phpstan/extension-installer": true
}
},
"provide": {
"psr/clock-implementation": "1.0"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>lcobucci/.github:renovate-config"
]
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Lcobucci\Clock;
use DateTimeImmutable;
use StellaMaris\Clock\ClockInterface;
interface Clock extends ClockInterface
{
public function now(): DateTimeImmutable;
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Lcobucci\Clock;
use DateTimeImmutable;
use DateTimeZone;
final class FrozenClock implements Clock
{
public function __construct(private DateTimeImmutable $now)
{
}
public static function fromUTC(): self
{
return new self(new DateTimeImmutable('now', new DateTimeZone('UTC')));
}
public function setTo(DateTimeImmutable $now): void
{
$this->now = $now;
}
public function now(): DateTimeImmutable
{
return $this->now;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Lcobucci\Clock;
use DateTimeImmutable;
use DateTimeZone;
use function date_default_timezone_get;
final class SystemClock implements Clock
{
public function __construct(private readonly DateTimeZone $timezone)
{
}
public static function fromUTC(): self
{
return new self(new DateTimeZone('UTC'));
}
public static function fromSystemTimezone(): self
{
return new self(new DateTimeZone(date_default_timezone_get()));
}
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('now', $this->timezone);
}
}

View File

@ -0,0 +1,27 @@
Copyright (c) 2014, Luís Cobucci
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the {organization} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,62 @@
{
"name": "lcobucci/jwt",
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"type": "library",
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"keywords": [
"JWT",
"JWS"
],
"license": [
"BSD-3-Clause"
],
"require": {
"php": "^7.4 || ^8.0",
"ext-mbstring": "*",
"ext-openssl": "*",
"lcobucci/clock": "^2.0"
},
"require-dev": {
"infection/infection": "^0.20",
"lcobucci/coding-standard": "^6.0",
"mikey179/vfsstream": "^1.6",
"phpbench/phpbench": "^0.17",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/php-invoker": "^3.1",
"phpunit/phpunit": "^9.4"
},
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Lcobucci\\JWT\\": [
"test/_keys",
"test/unit",
"test/performance"
],
"Lcobucci\\JWT\\FunctionalTests\\": "test/functional"
}
},
"config": {
"preferred-install": "dist",
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
}
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT;
use DateTimeImmutable;
use Lcobucci\JWT\Encoding\CannotEncodeContent;
use Lcobucci\JWT\Signer\CannotSignPayload;
use Lcobucci\JWT\Signer\Ecdsa\ConversionFailed;
use Lcobucci\JWT\Signer\InvalidKeyProvided;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Token\Plain;
use Lcobucci\JWT\Token\RegisteredClaimGiven;
interface Builder
{
/**
* Appends new items to audience
*/
public function permittedFor(string ...$audiences): Builder;
/**
* Configures the expiration time
*/
public function expiresAt(DateTimeImmutable $expiration): Builder;
/**
* Configures the token id
*/
public function identifiedBy(string $id): Builder;
/**
* Configures the time that the token was issued
*/
public function issuedAt(DateTimeImmutable $issuedAt): Builder;
/**
* Configures the issuer
*/
public function issuedBy(string $issuer): Builder;
/**
* Configures the time before which the token cannot be accepted
*/
public function canOnlyBeUsedAfter(DateTimeImmutable $notBefore): Builder;
/**
* Configures the subject
*/
public function relatedTo(string $subject): Builder;
/**
* Configures a header item
*
* @param mixed $value
*/
public function withHeader(string $name, $value): Builder;
/**
* Configures a claim item
*
* @param mixed $value
*
* @throws RegisteredClaimGiven When trying to set a registered claim.
*/
public function withClaim(string $name, $value): Builder;
/**
* Returns a signed token to be used
*
* @throws CannotEncodeContent When data cannot be converted to JSON.
* @throws CannotSignPayload When payload signing fails.
* @throws InvalidKeyProvided When issue key is invalid/incompatible.
* @throws ConversionFailed When signature could not be converted.
*/
public function getToken(Signer $signer, Key $key): Plain;
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT;
interface ClaimsFormatter
{
/**
* @param array<string, mixed> $claims
*
* @return array<string, mixed>
*/
public function formatClaims(array $claims): array;
}

View File

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT;
use Closure;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\None;
use Lcobucci\JWT\Validation\Constraint;
/**
* Configuration container for the JWT Builder and Parser
*
* Serves like a small DI container to simplify the creation and usage
* of the objects.
*/
final class Configuration
{
private Parser $parser;
private Signer $signer;
private Key $signingKey;
private Key $verificationKey;
private Validator $validator;
/** @var Closure(ClaimsFormatter $claimFormatter): Builder */
private Closure $builderFactory;
/** @var Constraint[] */
private array $validationConstraints = [];
private function __construct(
Signer $signer,
Key $signingKey,
Key $verificationKey,
?Encoder $encoder = null,
?Decoder $decoder = null
) {
$this->signer = $signer;
$this->signingKey = $signingKey;
$this->verificationKey = $verificationKey;
$this->parser = new Token\Parser($decoder ?? new JoseEncoder());
$this->validator = new Validation\Validator();
$this->builderFactory = static function (ClaimsFormatter $claimFormatter) use ($encoder): Builder {
return new Token\Builder($encoder ?? new JoseEncoder(), $claimFormatter);
};
}
public static function forAsymmetricSigner(
Signer $signer,
Key $signingKey,
Key $verificationKey,
?Encoder $encoder = null,
?Decoder $decoder = null
): self {
return new self(
$signer,
$signingKey,
$verificationKey,
$encoder,
$decoder
);
}
public static function forSymmetricSigner(
Signer $signer,
Key $key,
?Encoder $encoder = null,
?Decoder $decoder = null
): self {
return new self(
$signer,
$key,
$key,
$encoder,
$decoder
);
}
public static function forUnsecuredSigner(
?Encoder $encoder = null,
?Decoder $decoder = null
): self {
$key = InMemory::empty();
return new self(
new None(),
$key,
$key,
$encoder,
$decoder
);
}
/** @param callable(ClaimsFormatter): Builder $builderFactory */
public function setBuilderFactory(callable $builderFactory): void
{
$this->builderFactory = Closure::fromCallable($builderFactory);
}
public function builder(?ClaimsFormatter $claimFormatter = null): Builder
{
return ($this->builderFactory)($claimFormatter ?? ChainedFormatter::default());
}
public function parser(): Parser
{
return $this->parser;
}
public function setParser(Parser $parser): void
{
$this->parser = $parser;
}
public function signer(): Signer
{
return $this->signer;
}
public function signingKey(): Key
{
return $this->signingKey;
}
public function verificationKey(): Key
{
return $this->verificationKey;
}
public function validator(): Validator
{
return $this->validator;
}
public function setValidator(Validator $validator): void
{
$this->validator = $validator;
}
/** @return Constraint[] */
public function validationConstraints(): array
{
return $this->validationConstraints;
}
public function setValidationConstraints(Constraint ...$validationConstraints): void
{
$this->validationConstraints = $validationConstraints;
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT;
use Lcobucci\JWT\Encoding\CannotDecodeContent;
interface Decoder
{
/**
* Decodes from JSON, validating the errors
*
* @return mixed
*
* @throws CannotDecodeContent When something goes wrong while decoding.
*/
public function jsonDecode(string $json);
/**
* Decodes from Base64URL
*
* @link http://tools.ietf.org/html/rfc4648#section-5
*
* @throws CannotDecodeContent When something goes wrong while decoding.
*/
public function base64UrlDecode(string $data): string;
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT;
use Lcobucci\JWT\Encoding\CannotEncodeContent;
interface Encoder
{
/**
* Encodes to JSON, validating the errors
*
* @param mixed $data
*
* @throws CannotEncodeContent When something goes wrong while encoding.
*/
public function jsonEncode($data): string;
/**
* Encodes to base64url
*
* @link http://tools.ietf.org/html/rfc4648#section-5
*/
public function base64UrlEncode(string $data): string;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT\Encoding;
use JsonException;
use Lcobucci\JWT\Exception;
use RuntimeException;
final class CannotDecodeContent extends RuntimeException implements Exception
{
public static function jsonIssues(JsonException $previous): self
{
return new self('Error while decoding from JSON', 0, $previous);
}
public static function invalidBase64String(): self
{
return new self('Error while decoding from Base64Url, invalid base64 characters detected');
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT\Encoding;
use JsonException;
use Lcobucci\JWT\Exception;
use RuntimeException;
final class CannotEncodeContent extends RuntimeException implements Exception
{
public static function jsonIssues(JsonException $previous): self
{
return new self('Error while encoding to JSON', 0, $previous);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT\Encoding;
use Lcobucci\JWT\ClaimsFormatter;
final class ChainedFormatter implements ClaimsFormatter
{
/** @var list<ClaimsFormatter> */
private array $formatters;
public function __construct(ClaimsFormatter ...$formatters)
{
$this->formatters = $formatters;
}
public static function default(): self
{
return new self(new UnifyAudience(), new MicrosecondBasedDateConversion());
}
/** @inheritdoc */
public function formatClaims(array $claims): array
{
foreach ($this->formatters as $formatter) {
$claims = $formatter->formatClaims($claims);
}
return $claims;
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Lcobucci\JWT\Encoding;
use JsonException;
use Lcobucci\JWT\Decoder;
use Lcobucci\JWT\Encoder;
use function base64_decode;
use function base64_encode;
use function is_string;
use function json_decode;
use function json_encode;
use function rtrim;
use function strtr;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
/**
* A utilitarian class that encodes and decodes data according with JOSE specifications
*/
final class JoseEncoder implements Encoder, Decoder
{
private const JSON_DEFAULT_DEPTH = 512;
/** @inheritdoc */
public function jsonEncode($data): string
{
try {
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw CannotEncodeContent::jsonIssues($exception);
}
}
/** @inheritdoc */
public function jsonDecode(string $json)
{
try {
return json_decode($json, true, self::JSON_DEFAULT_DEPTH, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw CannotDecodeContent::jsonIssues($exception);
}
}
public function base64UrlEncode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
public function base64UrlDecode(string $data): string
{
// Padding isn't added back because it isn't strictly necessary for decoding with PHP
$decodedContent = base64_decode(strtr($data, '-_', '+/'), true);
if (! is_string($decodedContent)) {
throw CannotDecodeContent::invalidBase64String();
}
return $decodedContent;
}
}

Some files were not shown because too many files have changed in this diff Show More