Skip to content

Repository files navigation

Locker Secrets PHP SDK

The official PHP SDK for Locker Passwords & Secrets Management.

This package is a typed PHP client for the stable Locker SDK protocol. It starts the Locker CLI as locker sdk, sends one JSON-RPC 2.0 request over stdin, reads one response from stdout, and never depends on human-facing CLI output.

Requirements

  • PHP 8.2 or later
  • The JSON and Sodium extensions
  • Linux or macOS with the pcntl and posix extensions
  • The Locker CLI with SDK protocol v1 support
  • A Locker access key ID and secret access key

The package has no third-party runtime dependency. Sodium verifies the Locker release signatures with the public key embedded in the SDK package.

Upgrading from the unfinished legacy package line is a breaking change. See MIGRATION.md.

Installation

composer require lockerpm/locker-secrets

Install the Locker CLI through the signed release channel, or configure the exact path to a trusted binary:

use Locker\ClientOptions;
use Locker\Locker;

$locker = new Locker(
    accessKeyId: $accessKeyId,
    secretAccessKey: $secretAccessKey,
    options: new ClientOptions(cliPath: '/opt/locker/bin/locker'),
);

Binary resolution is deterministic:

  1. ClientOptions::$cliPath
  2. LOCKER_CLI_PATH
  3. the managed binary from Locker's signed release channel

Explicit paths intentionally bypass managed updates, but must be absolute, regular, executable non-link files. Bare and relative values are rejected instead of being searched through ambient PATH. In managed mode, the SDK checks for a compatible release on first use and at most once every six hours. A network failure may reuse a previously verified cache; invalid signatures, mutated releases, and rollback attempts always fail closed. A missing or invalid bundled trust root also fails closed; the SDK never executes an ambient locker from PATH. Verified releases, state, and the interprocess lock are isolated below ~/.locker/sdk-cli/php/, so another language SDK cannot overwrite this cache. On POSIX, every managed cache ancestor must be owned by the effective user and is revalidated at mode 0700 before updater state is used.

Legacy names such as locker_secret are never selected automatically. An explicit absolute path may use any filename, but protocol-v1 capability negotiation remains mandatory.

Scanner-compatible setup

The Locker scanner generates code against this public contract:

<?php

use Locker\Locker;

$locker = Locker::fromEnvironment();
$databasePassword = $locker->getRequired('DATABASE_PASSWORD');

fromEnvironment() reads the canonical variables:

Environment variable Purpose
LOCKER_ACCESS_KEY_ID Project access key ID as a UUIDv4
LOCKER_SECRET_ACCESS_KEY Non-empty canonical standard Base64 project secret access key
LOCKER_API_BASE Cloud or self-hosted API base URL
LOCKER_CLI_PATH Absolute caller-owned CLI path

For migration only, ACCESS_KEY_ID, SECRET_ACCESS_KEY, LOCKER_ACCESS_KEY_SECRET, and ACCESS_KEY_SECRET are accepted after the canonical names. Outer credential whitespace is trimmed once, then the pair is validated before any managed download, binary resolution, capability negotiation, or CLI launch. Malformed input therefore fails locally with a typed, non-retryable AuthenticationException; it is never sent to the CLI. Credentials are removed from the environment inherited by the child process.

Secrets

$secrets = $locker->secrets();

$secret = $secrets->get('DATABASE_PASSWORD', 'production');
$all = $secrets->list('production');

$page = $secrets->listPage(
    environment: 'production',
    pageSize: 100,
);
while ($page->nextCursor !== null) {
    $page = $secrets->listPage(
        environment: 'production',
        pageSize: 100,
        cursor: $page->nextCursor,
    );
}

$created = $secrets->create(
    key: 'API_TOKEN',
    value: $token,
    environment: 'production',
    description: 'Token used by the worker',
);

$updated = $secrets->update(
    key: 'API_TOKEN',
    changes: [
        'value' => $rotatedToken,
        'description' => '',
        'environment' => null,
    ],
    environment: 'production',
);

Empty secret values and descriptions are valid. In an update, 'environment' => null clears the environment association.

list() remains available for compatibility. Prefer listPage() for vaults that could exceed the CLI's negotiated response-size limit. Cursors are opaque; pass nextCursor back unchanged.

getRequired() returns the secret value and throws when the secret does not exist:

$token = $locker->getRequired('API_TOKEN', 'production');

Use getOrDefault() only when a missing secret has a safe fallback. It catches only Locker's numeric not_found error; authentication, permission, network, storage, server, and protocol failures still fail closed:

$region = $locker->getOrDefault('REGION', 'us-east-1');

Environments

$environments = $locker->environments();

$production = $environments->get('production');
$all = $environments->list();
$page = $environments->listPage(pageSize: 100);

$created = $environments->create(
    name: 'staging',
    externalUrl: 'https://staging.example.com',
    description: '',
);

$updated = $environments->update(
    name: 'staging',
    changes: [
        'name' => 'staging-eu',
        'external_url' => 'https://eu.staging.example.com',
    ],
);

As with secrets, use listPage() for potentially large environment collections.

Client options

use Locker\ClientOptions;
use Locker\Locker;

$options = new ClientOptions(
    cliPath: '/opt/locker/bin/locker',
    timeoutSeconds: 15.0,
    apiBase: 'https://api.locker.io/locker_secrets',
    headers: ['CF-Access-Client-Id' => $clientId],
    forceRefresh: false,
    cacheMaxAgeSeconds: 120,
);

$locker = new Locker($accessKeyId, $secretAccessKey, $options);

Custom headers, credentials, and mutation values are sent only in the JSON request body. They never appear in argv, child environment variables, debug output, or transport exception messages.

apiBase accepts an absolute HTTP or HTTPS base URL. Credential-bearing, query-bearing, fragmented, ambiguous, and control-character URLs are rejected; put custom access data in headers instead.

The SDK negotiates system.capabilities before its first vault operation and caches one capability result for a bounded time. Use $locker->capabilities(forceRefresh: true) to renegotiate explicitly. Negotiated request, response, and JSON-depth limits are enforced locally.

Timeout, retry, and vault cache

timeoutSeconds is one total initialization or operation budget. On POSIX, timeout terminates the CLI process group. The public synchronous API has no cooperative cancellation token; use the timeout and cancel the supervising request/process when application-level cancellation is required.

The SDK does not retry API or JSON-RPC failures. Create and update are issued once because a lost response can leave the remote commit outcome unknown. One internal rebind attempt is allowed only when the executable changes before a protocol exchange. Applications may inspect isRetryable() and add bounded retry only around read-only operations.

The SDK does not retain plaintext secret values. forceRefresh and cacheMaxAgeSeconds are delegated to the CLI's encrypted, revision-aware cache. A zero max age disables offline reuse; force refresh requires a successful server response. Authentication, authorization, TLS, integrity, malformed-response, and local-storage failures always fail closed.

Errors

All SDK exceptions implement Locker\Exception\ExceptionInterface. JSON-RPC codes map to these typed operation errors:

use Locker\Exception\AlreadyExistsException;
use Locker\Exception\ConflictException;

try {
    $locker->secrets()->create(
        key: 'PAYMENT_API_KEY',
        value: 'value',
    );
} catch (AlreadyExistsException $error) {
    // The secret or environment already exists.
    // AlreadyExistsException is also a ConflictException.
}
Code Exception Canonical kind
-32700 ProtocolException parse_error
-32600 ProtocolException invalid_request
-32601 ProtocolException method_not_found
-32602 ProtocolException invalid_params
-32603 ProtocolException internal_protocol_error
-32000 OperationException and legacy subtypes operation_error, request_rejected, response_too_large, cancelled
-32001 AuthenticationException missing_credentials, invalid_access_key_id, malformed_secret_access_key, invalid_secret_access_key, unauthorized
-32003 PermissionDeniedException forbidden; legacy permission_denied
-32004 NotFoundException secret_not_found, environment_not_found; legacy not-found aliases
-32009 ConflictException / AlreadyExistsException conflict, secret_already_exists, environment_already_exists
-32022 ValidationException validation_error
-32029 RateLimitedException rate_limited
-32050 NetworkException network_error, network_timeout; legacy http_error
-32051 ServerException service_unavailable, internal_error; legacy server_error
-32060 StorageException database_error, file_error, path_error
-32070 IntegrityException integrity, transport-integrity, and data-integrity kinds

Every RPC exception retains rpcCode(), requestId(), kind(), and isRetryable(). Protocol and transport failures use ProtocolException and TransportException. Classification is numeric-first. Distinctive kinds from older CLI releases (duplicate_hash, *_already_exists, conflict, validation_error, and the integrity aliases) also retain their typed mapping when the legacy code is -32000. request_rejected, response_too_large, and cancelled have explicit OperationException subtypes but are never guessed to be conflicts. Every -32000 error and known authentication, permission, not-found, conflict, validation, storage, integrity, protocol, cancellation, and internal-server error is non-retryable. Only rate-limit, network, service-unavailable, or an unknown server-range code may preserve a true hint. RateLimitedException::retryAfterSeconds() exposes an optional validated 0..86400 delay; the SDK does not retry vault RPCs automatically.

Authentication errors use stable, safe messages without echoing credentials or backend details. missing_credentials, invalid_access_key_id, and malformed_secret_access_key are detected locally before the CLI is resolved. A well-formed but mismatched pair is reported as invalid_secret_access_key; an upstream HTTP 401 is unauthorized. Unknown future authentication kinds remain an AuthenticationException with the generic authentication failed message.

The SDK sends context.error_contract = 'typed-v1' only after that exact contract is advertised in capability error_contracts. An absent list and unknown valid contracts remain compatible and do not opt in. serverRequestId() exposes a separately validated upstream correlation ID; it never replaces local JSON-RPC requestId() and is not included in default exception text.

Managed CLI installation and updates

The SDK embeds Locker's reviewed production Ed25519 release public key. To install or force-check the latest compatible CLI explicitly:

use Locker\Distribution\CliInstaller;

$path = (new CliInstaller())->installLatest();

The SDK accepts a managed download only after its signed release metadata, manifest, binary digest, detached signature, and executable platform header all verify. Verified versions are stored privately and activated atomically. A temporary release-channel outage may reuse a previously verified compatible binary; signature, schema, hash, platform, rollback, and local-integrity failures always fail closed.

Security properties

  • The only Locker CLI argv is [locker, sdk].
  • Request and response buffers, JSON depth, and process lifetime are bounded.
  • stdout and stderr are drained independently.
  • timeout handling terminates the full process tree. Linux and macOS run the CLI in a dedicated POSIX session and signal its process group. POSIX execution fails closed with a clear error unless the PHP pcntl and posix extensions are enabled.
  • timeoutSeconds is one end-to-end budget for signed managed resolution, update-lock waits, capability negotiation, and the vault operation. Managed cache contents are cryptographically rebound to the signed manifest before every CLI process starts. Tampering fails closed. Explicit absolute paths remain caller-owned and use the documented regular-file policy.
  • PHP's native proc_open() transport is fail-closed on Windows because PHP cannot provide bounded non-blocking pipes there. The SDK never persists protocol requests or responses, which contain credentials and secret values, to temporary files. Use the PHP SDK on Linux/macOS until a memory-only Windows transport is available.
  • JSON responses reject invalid UTF-8, duplicate object fields, trailing values, mismatched IDs, and malformed DTOs.
  • Unknown response fields are ignored for protocol-v1 forward compatibility.
  • Secret redacts its value from JSON, debug output, and string conversion.
  • SDK exception debug output excludes traces and previous exceptions. Set zend.exception_ignore_args=On in PHP configuration so PHP-generated stack traces also omit application call arguments that may contain secret values.
  • No CLI binary is bundled in the Composer package.
  • No network request occurs during Composer install or autoload. Client construction performs only the bounded periodic release check described above when managed resolution is active.
  • The managed updater pins a release public key, not a CLI version. Signed versions older than the verified installed release are rejected.

Versioning

The SDK follows Semantic Versioning. Packagist versions use MAJOR.MINOR.PATCH; matching source releases use vMAJOR.MINOR.PATCH. See the release notes when upgrading across a major version.

Support

For managed-install failures, check system time, access to the Locker Secrets download service, required POSIX extensions, and private ownership below ~/.locker/sdk-cli/php. For protocol errors, upgrade the SDK and CLI together or remove an incompatible explicit LOCKER_CLI_PATH. Never loosen cache permissions or log a secret-bearing request while troubleshooting.

License

Apache-2.0

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages